From d850c2e6913691115e4a0df64145a0fd527873ae Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 10 Jan 2019 10:52:56 +0100 Subject: [PATCH 01/86] [xcode] Add OPUS encoding profile for RTP Chromecasting --- src/transcode.c | 28 +++++++++++++++++++++------- src/transcode.h | 17 +++++++++++------ src/transcode_legacy.c | 27 +++++++++++++++++++-------- 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/src/transcode.c b/src/transcode.c index fa4ca863..252d41fa 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 @@ -221,6 +222,17 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile) 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_rate = 48000; + settings->channel_layout = AV_CH_LAYOUT_STEREO; + settings->channels = 2; + settings->sample_format = AV_SAMPLE_FMT_S16; // Only libopus support + settings->byte_depth = 2; // Bytes per sample = 16/8 + break; + case XCODE_JPEG: settings->encode_video = 1; settings->silent = 1; @@ -1383,7 +1395,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 +1426,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 +1501,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(enum transcode_profile profile, void *data, size_t size) { AVFrame *f; int ret; @@ -1502,7 +1514,7 @@ transcode_frame_new(enum transcode_profile profile, uint8_t *data, size_t size) return NULL; } - f->nb_samples = size / 4; + f->nb_samples = BTOS(size); f->format = AV_SAMPLE_FMT_S16; f->channel_layout = AV_CH_LAYOUT_STEREO; #ifdef HAVE_FFMPEG @@ -1511,7 +1523,9 @@ transcode_frame_new(enum transcode_profile profile, uint8_t *data, size_t size) 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)); @@ -1523,7 +1537,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; diff --git a/src/transcode.h b/src/transcode.h index 3e6cfc86..79e89268 100644 --- a/src/transcode.h +++ b/src/transcode.h @@ -14,6 +14,8 @@ enum transcode_profile XCODE_PCM16_HEADER, // 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, @@ -23,6 +25,8 @@ struct decode_ctx; struct encode_ctx; struct transcode_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); @@ -60,7 +64,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 +75,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 +91,18 @@ 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 * @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(enum transcode_profile profile, void *data, size_t size); void -transcode_frame_free(void *frame); +transcode_frame_free(transcode_frame *frame); /* Seek to the specified position - next transcode() will return this packet * diff --git a/src/transcode_legacy.c b/src/transcode_legacy.c index ef2a0b32..80421c29 100644 --- a/src/transcode_legacy.c +++ b/src/transcode_legacy.c @@ -39,6 +39,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 @@ -162,6 +163,16 @@ init_profile(struct encode_ctx *ctx, enum transcode_profile profile) ctx->byte_depth = 2; // Bytes per sample = 16/8 return 0; + case XCODE_OPUS: + ctx->format = "data"; // Means we get the raw packet from the encoder, no muxing + ctx->audio_codec = AV_CODEC_ID_OPUS; + ctx->sample_rate = 48000; + ctx->channel_layout = AV_CH_LAYOUT_STEREO; + ctx->channels = 2; + ctx->sample_format = AV_SAMPLE_FMT_S16; // Only libopus support + ctx->byte_depth = 2; // Bytes per sample = 16/8 + return 0; + case XCODE_MP3: ctx->format = "mp3"; ctx->audio_codec = AV_CODEC_ID_MP3; @@ -1337,7 +1348,7 @@ transcode_cleanup(struct transcode_ctx **ctx) } void -transcode_frame_free(void *frame) +transcode_frame_free(transcode_frame *frame) { struct decoded_frame *decoded = frame; @@ -1350,7 +1361,7 @@ transcode_frame_free(void *frame) int -transcode_decode(void **frame, struct decode_ctx *ctx) +transcode_decode(transcode_frame **frame, struct decode_ctx *ctx) { struct decoded_frame *decoded; AVPacket packet; @@ -1450,7 +1461,7 @@ transcode_decode(void **frame, struct decode_ctx *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) { struct decoded_frame *decoded = frame; int stream_index; @@ -1486,7 +1497,7 @@ transcode_encode(struct evbuffer *evbuf, struct encode_ctx *ctx, void *frame, in int transcode(struct evbuffer *evbuf, int *icy_timer, struct transcode_ctx *ctx, int want_bytes) { - void *frame; + transcode_frame *frame; int processed; int ret; @@ -1515,8 +1526,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(enum transcode_profile profile, void *data, size_t size) { struct decoded_frame *decoded; AVFrame *f; @@ -1540,7 +1551,7 @@ transcode_frame_new(enum transcode_profile profile, uint8_t *data, size_t size) decoded->stream_index = 0; decoded->frame = f; - f->nb_samples = size / 4; + f->nb_samples = BTOS(size); f->format = AV_SAMPLE_FMT_S16; f->channel_layout = AV_CH_LAYOUT_STEREO; #ifdef HAVE_FFMPEG @@ -1549,7 +1560,7 @@ transcode_frame_new(enum transcode_profile profile, uint8_t *data, size_t size) f->pts = AV_NOPTS_VALUE; f->sample_rate = 44100; - ret = avcodec_fill_audio_frame(f, 2, f->format, data, size, 0); + 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)); From 84e813038be5bc4000ce7a4ab2986304f213b604 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 11 Jan 2019 19:32:50 +0100 Subject: [PATCH 02/86] [player] Some additional comments --- src/player.h | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/player.h b/src/player.h index 3cae9c2d..cfec1800 100644 --- a/src/player.h +++ b/src/player.h @@ -7,14 +7,16 @@ #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 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 */ +// 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 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 { From 91825976053fdb49ce95b8f10038134bc62db62a Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 11 Jan 2019 19:34:36 +0100 Subject: [PATCH 03/86] [input/xcode] Write to input buffer with the sources native sample rate/format Still WIP at this point since the player and output can't use the use improved quality yet, and because rtptimes etc. are likely incorrect --- src/conffile.c | 2 + src/input.c | 4 +- src/input.h | 14 +++-- src/inputs/file_http.c | 13 ++++- src/inputs/pipe.c | 23 +++++++- src/spotify.c | 8 +-- src/transcode.c | 121 +++++++++++++++++++++++++++-------------- src/transcode.h | 22 +++++++- 8 files changed, 146 insertions(+), 61 deletions(-) diff --git a/src/conffile.c b/src/conffile.c index a0afe934..ca4bbbec 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() }; diff --git a/src/input.c b/src/input.c index 8c8dd3d5..63c6191a 100644 --- a/src/input.c +++ b/src/input.c @@ -215,7 +215,7 @@ playback(void *arg) // Loops until input_loop_break is set or no more input, e.g. EOF ret = inputs[type]->start(ps); if (ret < 0) - input_write(NULL, INPUT_FLAG_ERROR); + input_write(NULL, 0, 0, INPUT_FLAG_ERROR); #ifdef DEBUG DPRINTF(E_DBG, L_PLAYER, "Playback loop stopped (break is %d, ret %d)\n", input_loop_break, ret); @@ -240,7 +240,7 @@ input_wait(void) // Called by input modules from within the playback loop int -input_write(struct evbuffer *evbuf, short flags) +input_write(struct evbuffer *evbuf, int sample_rate, int bits_per_sample, short flags) { struct timespec ts; int ret; diff --git a/src/input.h b/src/input.h index c988d858..c4eafe9d 100644 --- a/src/input.h +++ b/src/input.h @@ -140,18 +140,20 @@ struct input_definition int input_loop_break; /* - * Transfer stream data to the player's input buffer. The input evbuf will be - * drained on succesful write. This is to avoid copying memory. If the player's - * input buffer is full the function will block until the write can be made - * (unless INPUT_FILE_NONBLOCK is set). + * 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. If the player's input buffer is full the function will block + * until the write can be made (unless INPUT_FILE_NONBLOCK is set). * - * @in evbuf Raw audio data to write + * @in evbuf Raw PCM_LE audio data to write + * @in evbuf Sample rate of the data + * @in evbuf Bits per sample (typically 16 or 24) * @in flags One or more INPUT_FLAG_* * @return 0 on success, EAGAIN if buffer was full (and _NONBLOCK is set), * -1 on error */ int -input_write(struct evbuffer *evbuf, short flags); +input_write(struct evbuffer *evbuf, int sample_rate, int bits_per_sample, short flags); /* * Input modules can use this to wait in the playback loop (like input_write() diff --git a/src/inputs/file_http.c b/src/inputs/file_http.c index 7422eacb..780ac50a 100644 --- a/src/inputs/file_http.c +++ b/src/inputs/file_http.c @@ -26,12 +26,13 @@ #include "transcode.h" #include "http.h" #include "misc.h" +#include "logger.h" #include "input.h" static int setup(struct player_source *ps) { - ps->input_ctx = transcode_setup(XCODE_PCM16_NOHEADER, ps->data_kind, ps->path, ps->len_ms, NULL); + ps->input_ctx = transcode_setup(XCODE_PCM_NATIVE, ps->data_kind, ps->path, ps->len_ms, NULL); if (!ps->input_ctx) return -1; @@ -57,27 +58,33 @@ setup_http(struct player_source *ps) static int start(struct player_source *ps) { + struct transcode_ctx *ctx = ps->input_ctx; struct evbuffer *evbuf; short flags; + int sample_rate; + int bps; int ret; int icy_timer; evbuf = evbuffer_new(); + sample_rate = transcode_encode_query(ctx->encode_ctx, "sample_rate"); + bps = transcode_encode_query(ctx->encode_ctx, "bits_per_sample"); + 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); + ret = transcode(evbuf, &icy_timer, ctx, 1); if (ret < 0) break; flags = ((ret == 0) ? INPUT_FLAG_EOF : 0) | (icy_timer ? INPUT_FLAG_METADATA : 0); - ret = input_write(evbuf, flags); + ret = input_write(evbuf, sample_rate, bps, flags); if (ret < 0) break; } diff --git a/src/inputs/pipe.c b/src/inputs/pipe.c index e1128912..95678ce5 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 @@ -307,7 +310,7 @@ parse_progress(struct input_metadata *m, char *progress) 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 + m->song_length = (end - start) * 1000 / pipe_sample_rate; } static void @@ -845,7 +848,7 @@ start(struct player_source *ps) ret = evbuffer_read(evbuf, pipe->fd, PIPE_READ_MAX); if ((ret == 0) && (pipe->is_autostarted)) { - input_write(evbuf, INPUT_FLAG_EOF); // Autostop + input_write(evbuf, pipe_sample_rate, pipe_bits_per_sample, INPUT_FLAG_EOF); // Autostop break; } else if ((ret == 0) || ((ret < 0) && (errno == EAGAIN))) @@ -862,7 +865,7 @@ start(struct player_source *ps) flags = (pipe_metadata_is_new ? INPUT_FLAG_METADATA : 0); pipe_metadata_is_new = 0; - ret = input_write(evbuf, flags); + ret = input_write(evbuf, pipe_sample_rate, pipe_bits_per_sample, flags); if (ret < 0) break; } @@ -945,6 +948,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; } diff --git a/src/spotify.c b/src/spotify.c index ed6d5745..b332f251 100644 --- a/src/spotify.c +++ b/src/spotify.c @@ -719,7 +719,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, 0, 0, INPUT_FLAG_EOF); *retval = 0; return COMMAND_END; @@ -1011,9 +1011,9 @@ static int music_delivery(sp_session *sess, const sp_audioformat *format, 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; } @@ -1037,7 +1037,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, format->sample_rate, 16, INPUT_FLAG_NONBLOCK); return num_frames; } diff --git a/src/transcode.c b/src/transcode.c index 252d41fa..4673b831 100644 --- a/src/transcode.c +++ b/src/transcode.c @@ -76,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; @@ -179,20 +177,12 @@ 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) { - const AVCodecDescriptor *codec_desc; - memset(settings, 0, sizeof(struct settings_ctx)); switch (profile) @@ -207,7 +197,13 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile) 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_PCM_NATIVE: // Sample rate and bit depth determined by source + settings->encode_audio = 1; + settings->channel_layout = AV_CH_LAYOUT_STEREO; + settings->channels = 2; settings->icy = 1; break; @@ -219,7 +215,6 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile) 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: @@ -230,7 +225,6 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile) settings->channel_layout = AV_CH_LAYOUT_STEREO; settings->channels = 2; settings->sample_format = AV_SAMPLE_FMT_S16; // Only libopus support - settings->byte_depth = 2; // Bytes per sample = 16/8 break; case XCODE_JPEG: @@ -253,18 +247,6 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile) return -1; } - if (settings->audio_codec) - { - codec_desc = avcodec_descriptor_get(settings->audio_codec); - settings->audio_codec_name = codec_desc->name; - } - - if (settings->video_codec) - { - codec_desc = avcodec_descriptor_get(settings->video_codec); - settings->video_codec_name = codec_desc->name; - } - return 0; } @@ -319,13 +301,15 @@ 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_bits_per_sample(ctx->settings.audio_codec); + wav_len = ctx->settings.channels * (bps / 8) * ctx->settings.sample_rate * (duration / 1000); *est_size = wav_len + sizeof(ctx->header); @@ -336,9 +320,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 / 8)); /* byte rate */ + add_le16(ctx->header + 32, ctx->settings.channels * (bps / 8)); /* block align */ + add_le16(ctx->header + 34, bps); /* bits per sample */ memcpy(ctx->header + 36, "data", 4); add_le32(ctx->header + 40, wav_len); } @@ -368,20 +352,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; } @@ -393,7 +384,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) @@ -406,7 +397,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; } @@ -415,7 +406,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; } @@ -888,14 +879,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; } @@ -1161,6 +1152,7 @@ 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; + int bps; CHECK_NULL(L_XCODE, ctx = calloc(1, sizeof(struct encode_ctx))); CHECK_NULL(L_XCODE, ctx->filt_frame = av_frame_alloc()); @@ -1172,6 +1164,26 @@ transcode_encode_setup(enum transcode_profile profile, struct decode_ctx *src_ct ctx->settings.width = width; ctx->settings.height = height; + if (!ctx->settings.sample_rate && ctx->settings.encode_audio) + ctx->settings.sample_rate = src_ctx->audio_stream.codec->sample_rate; + + if (!ctx->settings.sample_format && ctx->settings.encode_audio) + { + bps = av_get_bits_per_sample(src_ctx->audio_stream.codec->codec_id); + if (bps >= 24) + { + ctx->settings.sample_format = AV_SAMPLE_FMT_S32; + ctx->settings.audio_codec = AV_CODEC_ID_PCM_S24LE; + ctx->settings.format = "s24le"; + } + else + { + ctx->settings.sample_format = AV_SAMPLE_FMT_S16; + ctx->settings.audio_codec = AV_CODEC_ID_PCM_S16LE; + ctx->settings.format = "s16le"; + } + } + if (ctx->settings.wavheader) make_wav_header(ctx, src_ctx, est_size); @@ -1182,7 +1194,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_bits_per_sample(ctx->settings.audio_codec); + ctx->icy_interval = METADATA_ICY_INTERVAL * ctx->settings.channels * (bps / 8) * ctx->settings.sample_rate; + } return ctx; @@ -1223,6 +1238,7 @@ transcode_setup(enum transcode_profile profile, enum data_kind data_kind, const struct decode_ctx * transcode_decode_setup_raw(void) { + const AVCodecDescriptor *codec_desc; struct decode_ctx *ctx; AVCodec *decoder; int ret; @@ -1234,13 +1250,20 @@ transcode_decode_setup_raw(void) 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; } @@ -1255,7 +1278,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; } @@ -1659,6 +1682,24 @@ 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); + } + + return -1; +} + + /* Metadata */ struct http_icy_metadata * diff --git a/src/transcode.h b/src/transcode.h index 79e89268..333d7df0 100644 --- a/src/transcode.h +++ b/src/transcode.h @@ -8,10 +8,12 @@ enum transcode_profile { - // Transcodes the best audio stream into PCM16 (does not add wav header) + // Decodes/resamples the best audio stream into 44100 PCM16 (does not add wav header) XCODE_PCM16_NOHEADER, - // Transcodes the best audio stream into PCM16 (with wav header) + // Decodes/resamples the best audio stream into 44100 PCM16 (with wav header) XCODE_PCM16_HEADER, + // Decodes the best audio stream into PCM16 or PCM24, no resampling (does not add wav header) + XCODE_PCM_NATIVE, // Transcodes the best audio stream into MP3 XCODE_MP3, // Transcodes the best audio stream into OPUS @@ -23,7 +25,11 @@ 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; @@ -122,6 +128,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); From 1696fc33845281ea8b68243500b4e0de8f3b6169 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 14 Jan 2019 00:17:02 +0100 Subject: [PATCH 04/86] [input] Adapt input_buffer so it can handle dynamic quality (sample rates etc) Still WIP, player and outputs cannot handle this yet --- src/input.c | 198 ++++++++++++++++++++++++++--------------- src/input.h | 20 ++++- src/inputs/file_http.c | 9 +- src/inputs/pipe.c | 12 ++- src/spotify.c | 8 +- 5 files changed, 161 insertions(+), 86 deletions(-) diff --git a/src/input.c b/src/input.c index 63c6191a..35d8ecf6 100644 --- a/src/input.c +++ b/src/input.c @@ -40,6 +40,7 @@ #include "input.h" // Disallow further writes to the buffer when its size is larger than this threshold +// TODO untie from 44100 #define INPUT_BUFFER_THRESHOLD STOB(88200) // How long (in sec) to wait for player read before looping in playback thread #define INPUT_LOOP_TIMEOUT 1 @@ -64,21 +65,36 @@ static struct input_definition *inputs[] = { NULL }; +struct marker +{ + uint64_t pos; // Position of marker measured in bytes + struct input_quality quality; + enum input_flags flags; + + // 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 input_quality cur_write_quality; + struct input_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; @@ -101,47 +117,6 @@ static size_t debug_elapsed; /* ------------------------------ 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; -} - static int map_data_kind(int data_kind) { @@ -166,6 +141,27 @@ map_data_kind(int data_kind) } } +static void +marker_add(short flags) +{ + struct marker *head; + struct marker *marker; + + CHECK_NULL(L_PLAYER, marker = calloc(1, sizeof(struct marker))); + + marker->pos = input_buffer.bytes_written; + marker->quality = input_buffer.cur_write_quality; + marker->flags = flags; + + 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 int source_check_and_map(struct player_source *ps, const char *action, char check_setup) { @@ -215,7 +211,7 @@ playback(void *arg) // Loops until input_loop_break is set or no more input, e.g. EOF ret = inputs[type]->start(ps); if (ret < 0) - input_write(NULL, 0, 0, INPUT_FLAG_ERROR); + input_write(NULL, NULL, INPUT_FLAG_ERROR); #ifdef DEBUG DPRINTF(E_DBG, L_PLAYER, "Playback loop stopped (break is %d, ret %d)\n", input_loop_break, ret); @@ -240,7 +236,7 @@ input_wait(void) // Called by input modules from within the playback loop int -input_write(struct evbuffer *evbuf, int sample_rate, int bits_per_sample, short flags) +input_write(struct evbuffer *evbuf, struct input_quality *quality, short flags) { struct timespec ts; int ret; @@ -271,20 +267,31 @@ input_write(struct evbuffer *evbuf, int sample_rate, int bits_per_sample, short return 0; } + // Change of quality. Note, the marker is placed at the last position of the + // last byte we wrote, even though that of course doesn't have the new quality + // yet. Not intuitive, but input_read() will understand. + if (quality && memcmp(quality, &input_buffer.cur_write_quality, sizeof(struct input_quality)) != 0) + { + input_buffer.cur_write_quality = *quality; + marker_add(INPUT_FLAG_QUALITY); + } + + ret = 0; if (evbuf) - ret = evbuffer_add_buffer(input_buffer.evbuf, evbuf); - else - ret = 0; + { + input_buffer.bytes_written += evbuffer_get_length(evbuf); + ret = evbuffer_add_buffer(input_buffer.evbuf, evbuf); + if (ret < 0) + { + DPRINTF(E_LOG, L_PLAYER, "Error adding stream data to input buffer\n"); + 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); + // Note this marker is added at the post-write position, since EOF and ERROR + // belong there. We never want to add a marker for the NONBLOCK flag. + if (flags & ~INPUT_FLAG_NONBLOCK) + marker_add(flags); pthread_mutex_unlock(&input_buffer.mutex); @@ -298,6 +305,7 @@ input_write(struct evbuffer *evbuf, int sample_rate, int bits_per_sample, short int input_read(void *data, size_t size, short *flags) { + struct marker *marker; int len; *flags = 0; @@ -310,23 +318,50 @@ input_read(void *data, size_t size, short *flags) pthread_mutex_lock(&input_buffer.mutex); -#ifdef DEBUG - debug_elapsed += size; - if (debug_elapsed > STOB(441000)) // 10 sec + // 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) { - DPRINTF(E_DBG, L_PLAYER, "Input buffer has %zu bytes\n", evbuffer_get_length(input_buffer.evbuf)); - debug_elapsed = 0; + *flags = marker->flags; + if (*flags & INPUT_FLAG_QUALITY) + input_buffer.cur_read_quality = marker->quality; + + 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"); + *flags |= INPUT_FLAG_ERROR; goto out_unlock; } - *flags = flags_set(len); + input_buffer.bytes_read += len; + +#ifdef DEBUG + // Logs if flags present or each 10 seconds + debug_elapsed += len; + if (*flags || (debug_elapsed / STOB(input_buffer.cur_read_quality.sample_rate) > 10)) + { + debug_elapsed = 0; + DPRINTF(E_SPAM, L_PLAYER, "READ %zu bytes (%d/%d), WROTE %zu bytes (%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.bytes_written, + input_buffer.cur_write_quality.sample_rate, + input_buffer.cur_write_quality.bits_per_sample, + evbuffer_get_length(input_buffer.evbuf), + input_buffer.bytes_written - input_buffer.bytes_read, + *flags); + } +#endif out_unlock: pthread_cond_signal(&input_buffer.cond); @@ -465,19 +500,30 @@ input_seek(struct player_source *ps, int seek_ms) void input_flush(short *flags) { + struct marker *marker; size_t len; pthread_mutex_lock(&input_buffer.mutex); + // 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->flags; + input_buffer.marker_tail = marker->prev; + free(marker); + } + len = evbuffer_get_length(input_buffer.evbuf); evbuffer_drain(input_buffer.evbuf, len); - *flags = flags_set(len); + memset(&input_buffer.cur_read_quality, 0, sizeof(struct input_quality)); + memset(&input_buffer.cur_write_quality, 0, sizeof(struct input_quality)); + + input_buffer.bytes_read = 0; + input_buffer.bytes_written = 0; - input_buffer.error = 0; - input_buffer.eof = 0; - input_buffer.metadata = 0; input_buffer.full_cb = NULL; pthread_mutex_unlock(&input_buffer.mutex); @@ -487,6 +533,14 @@ input_flush(short *flags) #endif } +int +input_quality_get(struct input_quality *quality) +{ + // No mutex, other threads should not be able to affect cur_read_quality + *quality = input_buffer.cur_read_quality; + return 0; +} + int input_metadata_get(struct input_metadata *metadata, struct player_source *ps, int startup, uint64_t rtptime) { diff --git a/src/input.h b/src/input.h index c4eafe9d..72e5007a 100644 --- a/src/input.h +++ b/src/input.h @@ -29,6 +29,8 @@ enum input_flags INPUT_FLAG_ERROR = (1 << 2), // Flags possible new stream metadata INPUT_FLAG_METADATA = (1 << 3), + // Flags new stream quality + INPUT_FLAG_QUALITY = (1 << 4), }; struct player_source @@ -81,6 +83,13 @@ struct player_source typedef int (*input_cb)(void); +struct input_quality +{ + int sample_rate; + int bits_per_sample; + // Maybe some day also add channels here +}; + struct input_metadata { uint32_t item_id; @@ -146,14 +155,13 @@ int input_loop_break; * until the write can be made (unless INPUT_FILE_NONBLOCK is set). * * @in evbuf Raw PCM_LE audio data to write - * @in evbuf Sample rate of the data - * @in evbuf Bits per sample (typically 16 or 24) + * @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 */ int -input_write(struct evbuffer *evbuf, int sample_rate, int bits_per_sample, short flags); +input_write(struct evbuffer *evbuf, struct input_quality *quality, short flags); /* * Input modules can use this to wait in the playback loop (like input_write() @@ -224,6 +232,12 @@ input_seek(struct player_source *ps, int seek_ms); void input_flush(short *flags); +/* + * Returns the current quality of data returned by intput_read(). + */ +int +input_quality_get(struct input_quality *quality); + /* * Gets metadata from the input, returns 0 if metadata is set, otherwise -1 */ diff --git a/src/inputs/file_http.c b/src/inputs/file_http.c index 780ac50a..824280aa 100644 --- a/src/inputs/file_http.c +++ b/src/inputs/file_http.c @@ -59,17 +59,16 @@ static int start(struct player_source *ps) { struct transcode_ctx *ctx = ps->input_ctx; + struct input_quality quality = { 0 }; struct evbuffer *evbuf; short flags; - int sample_rate; - int bps; int ret; int icy_timer; evbuf = evbuffer_new(); - sample_rate = transcode_encode_query(ctx->encode_ctx, "sample_rate"); - bps = transcode_encode_query(ctx->encode_ctx, "bits_per_sample"); + quality.sample_rate = transcode_encode_query(ctx->encode_ctx, "sample_rate"); + quality.bits_per_sample = transcode_encode_query(ctx->encode_ctx, "bits_per_sample"); ret = -1; flags = 0; @@ -84,7 +83,7 @@ start(struct player_source *ps) flags = ((ret == 0) ? INPUT_FLAG_EOF : 0) | (icy_timer ? INPUT_FLAG_METADATA : 0); - ret = input_write(evbuf, sample_rate, bps, flags); + ret = input_write(evbuf, &quality, flags); if (ret < 0) break; } diff --git a/src/inputs/pipe.c b/src/inputs/pipe.c index 95678ce5..8c2cfaf2 100644 --- a/src/inputs/pipe.c +++ b/src/inputs/pipe.c @@ -831,6 +831,7 @@ static int start(struct player_source *ps) { struct pipe *pipe = ps->input_ctx; + struct input_quality quality = { 0 }; struct evbuffer *evbuf; short flags; int ret; @@ -842,13 +843,16 @@ start(struct player_source *ps) return -1; } + quality.sample_rate = pipe_sample_rate; + quality.bits_per_sample = pipe_bits_per_sample; + ret = -1; while (!input_loop_break) { ret = evbuffer_read(evbuf, pipe->fd, PIPE_READ_MAX); if ((ret == 0) && (pipe->is_autostarted)) { - input_write(evbuf, pipe_sample_rate, pipe_bits_per_sample, INPUT_FLAG_EOF); // Autostop + input_write(evbuf, NULL, INPUT_FLAG_EOF); // Autostop break; } else if ((ret == 0) || ((ret < 0) && (errno == EAGAIN))) @@ -865,7 +869,7 @@ start(struct player_source *ps) flags = (pipe_metadata_is_new ? INPUT_FLAG_METADATA : 0); pipe_metadata_is_new = 0; - ret = input_write(evbuf, pipe_sample_rate, pipe_bits_per_sample, flags); + ret = input_write(evbuf, &quality, flags); if (ret < 0) break; } @@ -949,14 +953,14 @@ init(void) } 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) + 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) + 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; diff --git a/src/spotify.c b/src/spotify.c index b332f251..74fa7b77 100644 --- a/src/spotify.c +++ b/src/spotify.c @@ -719,7 +719,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, 0, 0, INPUT_FLAG_EOF); + input_write(spotify_audio_buffer, NULL, INPUT_FLAG_EOF); *retval = 0; return COMMAND_END; @@ -1007,6 +1007,7 @@ logged_out(sp_session *sess) static int music_delivery(sp_session *sess, const sp_audioformat *format, const void *frames, int num_frames) { + struct input_quality quality = { 0 }; size_t size; int ret; @@ -1018,6 +1019,9 @@ static int music_delivery(sp_session *sess, const sp_audioformat *format, return num_frames; } + quality.sample_rate = format->sample_rate; + quality.bits_per_sample = 16; + // 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, format->sample_rate, 16, INPUT_FLAG_NONBLOCK); + input_write(spotify_audio_buffer, &quality, INPUT_FLAG_NONBLOCK); return num_frames; } From ad77a42dbba74d706ec10c0df21db50395874944 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 8 Feb 2019 18:53:40 +0100 Subject: [PATCH 05/86] [xcode] Adapt to allow for new raw PCM input profiles, e.g. 48000/16 --- src/transcode.c | 77 ++++++++++++++++++++++++++++++++++++++++++------- src/transcode.h | 23 ++++++++++----- 2 files changed, 82 insertions(+), 18 deletions(-) diff --git a/src/transcode.c b/src/transcode.c index 4673b831..74146f70 100644 --- a/src/transcode.c +++ b/src/transcode.c @@ -187,9 +187,16 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile) switch (profile) { + case XCODE_PCM_NATIVE: // Sample rate and bit depth determined by source + settings->encode_audio = 1; + settings->channel_layout = AV_CH_LAYOUT_STEREO; + settings->channels = 2; + settings->icy = 1; + break; + case XCODE_PCM16_HEADER: settings->wavheader = 1; - case XCODE_PCM16_NOHEADER: + case XCODE_PCM16_44100: settings->encode_audio = 1; settings->format = "s16le"; settings->audio_codec = AV_CODEC_ID_PCM_S16LE; @@ -197,14 +204,36 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile) settings->channel_layout = AV_CH_LAYOUT_STEREO; settings->channels = 2; settings->sample_format = AV_SAMPLE_FMT_S16; - settings->icy = 1; break; - case XCODE_PCM_NATIVE: // Sample rate and bit depth determined by source + case XCODE_PCM16_48000: settings->encode_audio = 1; + settings->format = "s16le"; + settings->audio_codec = AV_CODEC_ID_PCM_S16LE; + settings->sample_rate = 48000; settings->channel_layout = AV_CH_LAYOUT_STEREO; settings->channels = 2; - settings->icy = 1; + settings->sample_format = AV_SAMPLE_FMT_S16; + break; + + case XCODE_PCM24_44100: + settings->encode_audio = 1; + settings->format = "s24le"; + settings->audio_codec = AV_CODEC_ID_PCM_S24LE; + settings->sample_rate = 44100; + settings->channel_layout = AV_CH_LAYOUT_STEREO; + settings->channels = 2; + settings->sample_format = AV_SAMPLE_FMT_S32; + break; + + case XCODE_PCM24_48000: + settings->encode_audio = 1; + settings->format = "s24le"; + settings->audio_codec = AV_CODEC_ID_PCM_S24LE; + settings->sample_rate = 48000; + settings->channel_layout = AV_CH_LAYOUT_STEREO; + settings->channels = 2; + settings->sample_format = AV_SAMPLE_FMT_S32; break; case XCODE_MP3: @@ -975,6 +1004,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, @@ -987,6 +1018,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) { @@ -1236,7 +1269,7 @@ 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) { const AVCodecDescriptor *codec_desc; struct decode_ctx *ctx; @@ -1245,7 +1278,7 @@ transcode_decode_setup_raw(void) 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) < 0) { goto out_free_ctx; } @@ -1408,6 +1441,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); @@ -1525,7 +1561,7 @@ transcode(struct evbuffer *evbuf, int *icy_timer, struct transcode_ctx *ctx, int } transcode_frame * -transcode_frame_new(enum transcode_profile profile, void *data, size_t size) +transcode_frame_new(void *data, size_t size, int nsamples, int sample_rate, int bits_per_sample) { AVFrame *f; int ret; @@ -1537,21 +1573,35 @@ transcode_frame_new(enum transcode_profile profile, void *data, size_t size) return NULL; } - f->nb_samples = BTOS(size); - f->format = AV_SAMPLE_FMT_S16; + if (bits_per_sample == 16) + { + f->format = AV_SAMPLE_FMT_S16; + } + else if (bits_per_sample == 24) + { + f->format = AV_SAMPLE_FMT_S32; + } + else + { + DPRINTF(E_LOG, L_XCODE, "transcode_frame_new() called with unsupported bps (%d)\n", bits_per_sample); + av_frame_free(&f); + return NULL; + } + + f->sample_rate = sample_rate; + f->nb_samples = nsamples; f->channel_layout = AV_CH_LAYOUT_STEREO; #ifdef HAVE_FFMPEG f->channels = 2; #endif f->pts = AV_NOPTS_VALUE; - f->sample_rate = 44100; // 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, sample_rate, bits_per_sample, err2str(ret)); av_frame_free(&f); return NULL; } @@ -1695,6 +1745,11 @@ transcode_encode_query(struct encode_ctx *ctx, const char *query) 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; } diff --git a/src/transcode.h b/src/transcode.h index 333d7df0..3ce05d26 100644 --- a/src/transcode.h +++ b/src/transcode.h @@ -8,12 +8,17 @@ enum transcode_profile { - // Decodes/resamples the best audio stream into 44100 PCM16 (does not add wav header) - XCODE_PCM16_NOHEADER, - // Decodes/resamples the best audio stream into 44100 PCM16 (with wav header) - XCODE_PCM16_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 44100 PCM16 (with wav header) + XCODE_PCM16_HEADER, + // Decodes/resamples the best audio stream (no wav headers) + XCODE_PCM16_44100, + XCODE_PCM16_48000, + XCODE_PCM24_44100, + XCODE_PCM24_48000, // Transcodes the best audio stream into MP3 XCODE_MP3, // Transcodes the best audio stream into OPUS @@ -44,7 +49,7 @@ 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 decode_ctx * -transcode_decode_setup_raw(void); +transcode_decode_setup_raw(enum transcode_profile profile); int transcode_needed(const char *user_agent, const char *client_codecs, char *file_codectype); @@ -100,13 +105,17 @@ transcode(struct evbuffer *evbuf, int *icy_timer, struct transcode_ctx *ctx, int * 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 sample_rate + * Sample rate + * @in bits_per_sample + * BPS must be either 16 or 24 * @return Opaque pointer to frame if OK, otherwise NULL */ transcode_frame * -transcode_frame_new(enum transcode_profile profile, void *data, size_t size); +transcode_frame_new(void *data, size_t size, int nsamples, int sample_rate, int bits_per_sample); void transcode_frame_free(transcode_frame *frame); From 462d787fab22a1a12a8af6f98c234f2792cbb0b1 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 8 Feb 2019 18:58:46 +0100 Subject: [PATCH 06/86] [misc] Add media_quality struct for storing sample rate, bps and channels --- src/misc.c | 5 +++++ src/misc.h | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/misc.c b/src/misc.c index 2262c7f2..e65ce35a 100644 --- a/src/misc.c +++ b/src/misc.c @@ -1039,6 +1039,11 @@ murmur_hash64(const void *key, int len, uint32_t seed) # error Platform not supported #endif +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) diff --git a/src/misc.h b/src/misc.h index 0fb6416f..cd88ea05 100644 --- a/src/misc.h +++ b/src/misc.h @@ -12,11 +12,18 @@ #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]))) +// 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; @@ -114,6 +121,9 @@ b64_encode(const uint8_t *in, size_t len); uint64_t murmur_hash64(const void *key, int len, uint32_t seed); +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); From a9085e978d2ceb4aacecda4eab839c9e1d598d05 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 8 Feb 2019 19:03:09 +0100 Subject: [PATCH 07/86] [input] Change input abstraction to use media_quality instead of input_quality --- src/input.c | 25 ++++++++++++++----------- src/input.h | 12 +++--------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/input.c b/src/input.c index 35d8ecf6..9bdea003 100644 --- a/src/input.c +++ b/src/input.c @@ -41,7 +41,7 @@ // Disallow further writes to the buffer when its size is larger than this threshold // TODO untie from 44100 -#define INPUT_BUFFER_THRESHOLD STOB(88200) +#define INPUT_BUFFER_THRESHOLD STOB(88200, 16, 2) // How long (in sec) to wait for player read before looping in playback thread #define INPUT_LOOP_TIMEOUT 1 @@ -68,7 +68,7 @@ static struct input_definition *inputs[] = { struct marker { uint64_t pos; // Position of marker measured in bytes - struct input_quality quality; + struct media_quality quality; enum input_flags flags; // Reverse linked list, yay! @@ -89,8 +89,8 @@ struct input_buffer input_cb full_cb; // Quality of write/read data - struct input_quality cur_write_quality; - struct input_quality cur_read_quality; + struct media_quality cur_write_quality; + struct media_quality cur_read_quality; size_t bytes_written; size_t bytes_read; @@ -236,7 +236,7 @@ input_wait(void) // Called by input modules from within the playback loop int -input_write(struct evbuffer *evbuf, struct input_quality *quality, short flags) +input_write(struct evbuffer *evbuf, struct media_quality *quality, short flags) { struct timespec ts; int ret; @@ -270,7 +270,7 @@ input_write(struct evbuffer *evbuf, struct input_quality *quality, short flags) // Change of quality. Note, the marker is placed at the last position of the // last byte we wrote, even though that of course doesn't have the new quality // yet. Not intuitive, but input_read() will understand. - if (quality && memcmp(quality, &input_buffer.cur_write_quality, sizeof(struct input_quality)) != 0) + if (quality && !quality_is_equal(quality, &input_buffer.cur_write_quality)) { input_buffer.cur_write_quality = *quality; marker_add(INPUT_FLAG_QUALITY); @@ -346,17 +346,20 @@ input_read(void *data, size_t size, short *flags) #ifdef DEBUG // Logs if flags present or each 10 seconds + 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 (*flags || (debug_elapsed / STOB(input_buffer.cur_read_quality.sample_rate) > 10)) + if (*flags || (debug_elapsed > 10 * one_sec_size)) { debug_elapsed = 0; - DPRINTF(E_SPAM, L_PLAYER, "READ %zu bytes (%d/%d), WROTE %zu bytes (%d/%d), SIZE %zu (=%zu), FLAGS %04x\n", + DPRINTF(E_SPAM, 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, *flags); @@ -518,8 +521,8 @@ input_flush(short *flags) evbuffer_drain(input_buffer.evbuf, len); - memset(&input_buffer.cur_read_quality, 0, sizeof(struct input_quality)); - memset(&input_buffer.cur_write_quality, 0, sizeof(struct input_quality)); + 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; @@ -534,7 +537,7 @@ input_flush(short *flags) } int -input_quality_get(struct input_quality *quality) +input_quality_get(struct media_quality *quality) { // No mutex, other threads should not be able to affect cur_read_quality *quality = input_buffer.cur_read_quality; diff --git a/src/input.h b/src/input.h index 72e5007a..aa7120cf 100644 --- a/src/input.h +++ b/src/input.h @@ -6,6 +6,7 @@ # include #endif #include +#include "misc.h" #include "transcode.h" // Must be in sync with inputs[] in input.c @@ -83,13 +84,6 @@ struct player_source typedef int (*input_cb)(void); -struct input_quality -{ - int sample_rate; - int bits_per_sample; - // Maybe some day also add channels here -}; - struct input_metadata { uint32_t item_id; @@ -161,7 +155,7 @@ int input_loop_break; * -1 on error */ int -input_write(struct evbuffer *evbuf, struct input_quality *quality, 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() @@ -236,7 +230,7 @@ input_flush(short *flags); * Returns the current quality of data returned by intput_read(). */ int -input_quality_get(struct input_quality *quality); +input_quality_get(struct media_quality *quality); /* * Gets metadata from the input, returns 0 if metadata is set, otherwise -1 From 7048a72d86156a1ec70e3997c16240b9346bef18 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 8 Feb 2019 19:04:11 +0100 Subject: [PATCH 08/86] [spotify] Use media_quality instead of input_quality and allow more than 2 channels ... probably wouldn't actually work, hard to test --- src/spotify.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/spotify.c b/src/spotify.c index 74fa7b77..22f5e255 100644 --- a/src/spotify.c +++ b/src/spotify.c @@ -1007,7 +1007,7 @@ logged_out(sp_session *sess) static int music_delivery(sp_session *sess, const sp_audioformat *format, const void *frames, int num_frames) { - struct input_quality quality = { 0 }; + struct media_quality quality = { 0 }; size_t size; int ret; @@ -1021,6 +1021,7 @@ static int music_delivery(sp_session *sess, const sp_audioformat *format, quality.sample_rate = format->sample_rate; quality.bits_per_sample = 16; + quality.channels = format->channels; // Audio discontinuity, e.g. seek if (num_frames == 0) From 4fe5c47526595b10115120fd41632e577e2bebc8 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 8 Feb 2019 19:32:48 +0100 Subject: [PATCH 09/86] [inputs] Change pipe and file_http inputs to use media_quality --- src/inputs/file_http.c | 3 ++- src/inputs/pipe.c | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/inputs/file_http.c b/src/inputs/file_http.c index 824280aa..4f2743f1 100644 --- a/src/inputs/file_http.c +++ b/src/inputs/file_http.c @@ -59,7 +59,7 @@ static int start(struct player_source *ps) { struct transcode_ctx *ctx = ps->input_ctx; - struct input_quality quality = { 0 }; + struct media_quality quality = { 0 }; struct evbuffer *evbuf; short flags; int ret; @@ -69,6 +69,7 @@ start(struct player_source *ps) quality.sample_rate = transcode_encode_query(ctx->encode_ctx, "sample_rate"); quality.bits_per_sample = transcode_encode_query(ctx->encode_ctx, "bits_per_sample"); + quality.channels = transcode_encode_query(ctx->encode_ctx, "channels"); ret = -1; flags = 0; diff --git a/src/inputs/pipe.c b/src/inputs/pipe.c index 8c2cfaf2..b37308bc 100644 --- a/src/inputs/pipe.c +++ b/src/inputs/pipe.c @@ -831,7 +831,7 @@ static int start(struct player_source *ps) { struct pipe *pipe = ps->input_ctx; - struct input_quality quality = { 0 }; + struct media_quality quality = { 0 }; struct evbuffer *evbuf; short flags; int ret; @@ -845,6 +845,7 @@ start(struct player_source *ps) quality.sample_rate = pipe_sample_rate; quality.bits_per_sample = pipe_bits_per_sample; + quality.channels = 2; ret = -1; while (!input_loop_break) From 76bbfb6d2cc086cb15592e300629b84326283963 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 8 Feb 2019 20:04:50 +0100 Subject: [PATCH 10/86] [streaming] Adjust httpd_streaming to new transcode interface --- src/httpd_streaming.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/httpd_streaming.c b/src/httpd_streaming.c index 547b624d..daccb01c 100644 --- a/src/httpd_streaming.c +++ b/src/httpd_streaming.c @@ -44,8 +44,10 @@ 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 samples we store in the buffer used for transmitting from player to httpd thread +#define STREAMING_RAWBUF_SAMPLES 352 +// Buffer size +#define STREAMING_RAWBUF_SIZE (STOB(STREAMING_RAWBUF_SAMPLES, 16, 2)) // Linked list of mp3 streaming requests struct streaming_session { @@ -134,7 +136,7 @@ streaming_send_cb(evutil_socket_t fd, short event, void *arg) if (!streaming_sessions) return; - frame = transcode_frame_new(XCODE_MP3, streaming_rawbuf, STREAMING_RAWBUF_SIZE); + frame = transcode_frame_new(streaming_rawbuf, STREAMING_RAWBUF_SIZE, STREAMING_RAWBUF_SAMPLES, 44100, 16); if (!frame) { DPRINTF(E_LOG, L_STREAMING, "Could not convert raw PCM to frame\n"); @@ -289,7 +291,7 @@ streaming_init(void) int remaining; int ret; - decode_ctx = transcode_decode_setup_raw(); + decode_ctx = transcode_decode_setup_raw(XCODE_PCM16_44100); if (!decode_ctx) { DPRINTF(E_LOG, L_STREAMING, "Could not create decoding context\n"); @@ -339,10 +341,10 @@ streaming_init(void) } // Encode some silence which will be used for playback pause and put in a permanent buffer - remaining = STREAMING_SILENCE_INTERVAL * STOB(44100); + remaining = STREAMING_SILENCE_INTERVAL * STOB(44100, 16, 2); while (remaining > STREAMING_RAWBUF_SIZE) { - frame = transcode_frame_new(XCODE_MP3, streaming_rawbuf, STREAMING_RAWBUF_SIZE); + frame = transcode_frame_new(streaming_rawbuf, STREAMING_RAWBUF_SIZE, STREAMING_RAWBUF_SAMPLES, 44100, 16); if (!frame) { DPRINTF(E_LOG, L_STREAMING, "Could not convert raw PCM to frame\n"); From 7e48887adc7ebdcea46ea3b5cd78441882184ddb Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 8 Feb 2019 20:07:45 +0100 Subject: [PATCH 11/86] [rtp] Add RTP utility module: rtp_common.c rtp_common.h Expectation is to use this for both Airplay and Chromecast RTP streaming --- src/Makefile.am | 1 + src/outputs/rtp_common.c | 313 +++++++++++++++++++++++++++++++++++++++ src/outputs/rtp_common.h | 93 ++++++++++++ 3 files changed, 407 insertions(+) create mode 100644 src/outputs/rtp_common.c create mode 100644 src/outputs/rtp_common.h diff --git a/src/Makefile.am b/src/Makefile.am index c441591e..01ae5f64 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -131,6 +131,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/outputs/rtp_common.c b/src/outputs/rtp_common.c new file mode 100644 index 00000000..0e8a8014 --- /dev/null +++ b/src/outputs/rtp_common.c @@ -0,0 +1,313 @@ +/* + * 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 "conffile.h" +#include "misc.h" +#include "player.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, int buffer_duration) +{ + 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; + + session->buffer_duration = buffer_duration; + + session->is_virgin = true; + + 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->sync_packet_next.data); + + free(session); +} + +void +rtp_session_restart(struct rtp_session *session, struct timespec *ts) +{ + session->is_virgin = true; + session->start_time = *ts; + 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) +{ + 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; + + // RTP Header + pkt->header[0] = 0x80; // Version = 2, P, X and CC are 0 + pkt->header[1] = (session->is_virgin) ? 0xe0 : 0x60; // TODO allow other payloads + + 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); + +/* DPRINTF(E_DBG, L_PLAYER, "RTP PACKET seqnum %u, rtptime %u, payload 0x%x, pktbuf_s %zu\n", + session->seqnum, + session->pos, + pkt->header[1], + session->pktbuf_len + ); +*/ + 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->is_virgin = false; +} + +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_check(struct rtp_session *session, struct rtp_packet *pkt) +{ + if (!session->sync_each_nsamples) + { + return false; + } + + if (session->sync_counter > session->sync_each_nsamples) + { + session->sync_counter = 0; + return true; + } + + session->sync_counter += pkt->samples; // TODO Should this move to a sync_commit function? + return false; +} + +struct rtp_packet * +rtp_sync_packet_next(struct rtp_session *session) +{ + struct timespec ts; + struct ntp_timestamp cur_stamp; + uint64_t elapsed_usec; + uint64_t elapsed_samples; + uint32_t rtptime; + uint32_t cur_pos; + int ret; + + 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; + } + + memset(session->sync_packet_next.data, 0, session->sync_packet_next.data_len); // TODO remove this and just zero byte 3 instead? + + session->sync_packet_next.data[0] = (session->is_virgin) ? 0x90 : 0x80; + session->sync_packet_next.data[1] = 0xd4; + session->sync_packet_next.data[3] = 0x07; + + if (session->is_virgin) + { + session->sync_last_check.pos = session->pos - session->buffer_duration * session->quality.sample_rate; + session->sync_last_check.ts = session->start_time; + timespec_to_ntp(&session->start_time, &cur_stamp); + } + else + { + ret = player_get_time(&ts); + if (ret < 0) + return NULL; + + elapsed_usec = (ts.tv_sec - session->sync_last_check.ts.tv_sec) * 1000000 + (ts.tv_nsec - session->sync_last_check.ts.tv_nsec) / 1000; + + // How many samples should have been played since last check + elapsed_samples = (elapsed_usec * session->quality.sample_rate) / 1000000; + + session->sync_last_check.pos += elapsed_samples; // TODO should updating sync_last_check move to a commit function? + session->sync_last_check.ts = ts; + timespec_to_ntp(&ts, &cur_stamp); + } + + cur_pos = htobe32(session->sync_last_check.pos); + memcpy(session->sync_packet_next.data + 4, &cur_pos, 4); + + cur_stamp.sec = htobe32(cur_stamp.sec); + cur_stamp.frac = htobe32(cur_stamp.frac); + memcpy(session->sync_packet_next.data + 8, &cur_stamp.sec, 4); + memcpy(session->sync_packet_next.data + 12, &cur_stamp.frac, 4); + + rtptime = htobe32(session->pos); + memcpy(session->sync_packet_next.data + 16, &rtptime, 4); + +/* DPRINTF(E_DBG, L_PLAYER, "SYNC PACKET ts:%ld.%ld, next_pkt:%u, cur_pos:%u, payload:0x%x, sync_counter:%d, init:%d\n", + ts.tv_sec, ts.tv_nsec, + session->pos, + session->sync_last_check.pos, + session->sync_packet_next.data[0], + session->sync_counter, + session->is_virgin + ); +*/ + 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..977412a3 --- /dev/null +++ b/src/outputs/rtp_common.h @@ -0,0 +1,93 @@ +#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; + + // True if we haven't started streaming yet + bool is_virgin; + + 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; + + // Time of playback start (given by player) + struct timespec start_time; + + // Number of seconds that we tell the client 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 buffer_duration; + + // 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 rtcp_timestamp sync_last_check; +}; + + +struct rtp_session * +rtp_session_new(struct media_quality *quality, int pktbuf_size, int sync_each_nsamples, int buffer_duration); + +void +rtp_session_free(struct rtp_session *session); + +void +rtp_session_restart(struct rtp_session *session, struct timespec *ts); + +struct rtp_packet * +rtp_packet_next(struct rtp_session *session, size_t payload_len, int samples); + +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_check(struct rtp_session *session, struct rtp_packet *pkt); + +struct rtp_packet * +rtp_sync_packet_next(struct rtp_session *session); + +#endif /* !__RTP_COMMON_H__ */ From cdd0aa884b2c52b242bc4a6e47a8481998f86fa8 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 8 Feb 2019 20:36:21 +0100 Subject: [PATCH 12/86] [outputs] Add ability to deal with multiple qualities Output module can now take input data in multiple quality levels, and can resample to those output modules that would require a certain quality level, like raop.c would --- src/outputs.c | 277 +++++++++++++++++++++++++++++++++++++++++++++++++- src/outputs.h | 58 ++++++++++- 2 files changed, 332 insertions(+), 3 deletions(-) diff --git a/src/outputs.c b/src/outputs.c index e269c761..873bb3fc 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -29,6 +30,8 @@ #include #include "logger.h" +#include "misc.h" +#include "transcode.h" #include "outputs.h" extern struct output_definition output_raop; @@ -63,6 +66,145 @@ static struct output_definition *outputs[] = { NULL }; +struct output_quality_subscription +{ + int count; + struct media_quality quality; + struct encode_ctx *encode_ctx; +}; + +// Last element is a zero terminator +static struct output_quality_subscription output_quality_subscriptions[OUTPUTS_MAX_QUALITY_SUBSCRIPTIONS + 1]; +static bool output_got_new_subscription; + +/* ------------------------------- MISC HELPERS ----------------------------- */ + +static enum transcode_profile +quality_to_xcode(struct media_quality *quality) +{ + if (quality->sample_rate == 44100 && quality->bits_per_sample == 16) + return XCODE_PCM16_44100; + if (quality->sample_rate == 44100 && quality->bits_per_sample == 24) + return XCODE_PCM24_44100; + if (quality->sample_rate == 48000 && quality->bits_per_sample == 16) + return XCODE_PCM16_48000; + if (quality->sample_rate == 48000 && quality->bits_per_sample == 24) + return XCODE_PCM24_48000; + + 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); + 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, 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) +{ + transcode_frame *frame; + int ret; + int i; + int n; + + obuf->write_counter++; + + // 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->frames[0].quality) || output_got_new_subscription) + { + encoding_reset(quality); + output_got_new_subscription = false; + } + + // The first element of the output_buffer is always just the raw input frame + // 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->frames[0].evbuf, buf, bufsize); + obuf->frames[0].buffer = buf; + obuf->frames[0].bufsize = bufsize; + obuf->frames[0].quality = *quality; + obuf->frames[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 + + frame = transcode_frame_new(buf, bufsize, nsamples, quality->sample_rate, quality->bits_per_sample); + if (!frame) + continue; + + ret = transcode_encode(obuf->frames[n].evbuf, output_quality_subscriptions[i].encode_ctx, frame, 0); + transcode_frame_free(frame); + if (ret < 0) + continue; + + obuf->frames[n].buffer = evbuffer_pullup(obuf->frames[n].evbuf, -1); + obuf->frames[n].bufsize = evbuffer_get_length(obuf->frames[n].evbuf); + obuf->frames[n].quality = output_quality_subscriptions[i].quality; + obuf->frames[n].samples = BTOS(obuf->frames[n].bufsize, obuf->frames[n].quality.bits_per_sample, obuf->frames[n].quality.channels); + n++; + } +} + +static void +buffer_drain(struct output_buffer *obuf) +{ + int i; + + for (i = 0; obuf->frames[i].buffer; i++) + { + evbuffer_drain(obuf->frames[i].evbuf, obuf->frames[i].bufsize); + obuf->frames[i].buffer = NULL; + obuf->frames[i].bufsize = 0; + // We don't reset quality and samples, would be a waste of time + } +} + +/* ----------------------------------- API ---------------------------------- */ + int outputs_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime) { @@ -144,8 +286,20 @@ outputs_device_volume_to_pct(struct output_device *device, const char *volume) return -1; } +int +outputs_device_quality_set(struct output_device *device, struct media_quality *quality) +{ + if (outputs[device->type]->disabled) + return -1; + + if (outputs[device->type]->quality_set) + return outputs[device->type]->quality_set(device, quality); + else + return -1; +} + void -outputs_playback_start(uint64_t next_pkt, struct timespec *ts) +outputs_playback_start(uint64_t next_pkt, struct timespec *start_time) { int i; @@ -155,7 +309,22 @@ outputs_playback_start(uint64_t next_pkt, struct timespec *ts) continue; if (outputs[i]->playback_start) - outputs[i]->playback_start(next_pkt, ts); + outputs[i]->playback_start(next_pkt, start_time); + } +} + +void +outputs_playback_start2(struct timespec *start_time) +{ + int i; + + for (i = 0; outputs[i]; i++) + { + if (outputs[i]->disabled) + continue; + + if (outputs[i]->playback_start2) + outputs[i]->playback_start2(start_time); } } @@ -189,6 +358,25 @@ outputs_write(uint8_t *buf, uint64_t rtptime) } } +void +outputs_write2(void *buf, size_t bufsize, struct media_quality *quality, int nsamples) +{ + int i; + + buffer_fill(&output_buffer, buf, bufsize, quality, nsamples); + + for (i = 0; outputs[i]; i++) + { + if (outputs[i]->disabled) + continue; + + if (outputs[i]->write2) + outputs[i]->write2(&output_buffer); + } + + buffer_drain(&output_buffer); +} + int outputs_flush(output_status_cb cb, uint64_t rtptime) { @@ -334,6 +522,77 @@ outputs_authorize(enum output_types type, const char *pin) outputs[type]->authorize(pin); } +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? + output_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; + + 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]; +} + int outputs_priority(struct output_device *device) { @@ -378,6 +637,9 @@ outputs_init(void) if (no_output) return -1; + for (i = 0; i < ARRAY_SIZE(output_buffer.frames); i++) + output_buffer.frames[i].evbuf = evbuffer_new(); + return 0; } @@ -394,5 +656,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.frames); i++) + evbuffer_free(output_buffer.frames[i].evbuf); } diff --git a/src/outputs.h b/src/outputs.h index 48d91ff1..07b11810 100644 --- a/src/outputs.h +++ b/src/outputs.h @@ -3,6 +3,8 @@ #define __OUTPUTS_H__ #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 @@ -46,6 +48,20 @@ * */ +// 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 + // Must be in sync with outputs[] in outputs.c enum output_types { @@ -113,6 +129,9 @@ struct output_device int volume; int relvol; + // Quality of audio output + struct media_quality quality; + // Address char *v4_address; char *v6_address; @@ -141,6 +160,22 @@ struct output_metadata struct output_metadata *next; }; +struct output_frame +{ + 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 output_frame frames[OUTPUTS_MAX_QUALITY_SUBSCRIPTIONS + 1]; +} output_buffer; + + typedef void (*output_status_cb)(struct output_device *device, struct output_session *session, enum output_device_state status); struct output_definition @@ -183,12 +218,17 @@ struct output_definition // Convert device internal representation of volume to our pct scale int (*device_volume_to_pct)(struct output_device *device, const char *volume); + // Request a change of quality from the device + int (*quality_set)(struct output_device *device, struct media_quality *quality); + // Start/stop playback on devices that were started void (*playback_start)(uint64_t next_pkt, struct timespec *ts); + void (*playback_start2)(struct timespec *start_time); void (*playback_stop)(void); // Write stream data to the output devices void (*write)(uint8_t *buf, uint64_t rtptime); + void (*write2)(struct output_buffer *buffer); // Flush all sessions, the return must be number of sessions pending the flush int (*flush)(output_status_cb cb, uint64_t rtptime); @@ -224,8 +264,15 @@ 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); +// TODO should this function have a callback? +int +outputs_device_quality_set(struct output_device *device, struct media_quality *quality); + void -outputs_playback_start(uint64_t next_pkt, struct timespec *ts); +outputs_playback_start(uint64_t next_pkt, struct timespec *start_time); + +void +outputs_playback_start2(struct timespec *start_time); void outputs_playback_stop(void); @@ -233,6 +280,9 @@ outputs_playback_stop(void); void outputs_write(uint8_t *buf, uint64_t rtptime); +void +outputs_write2(void *buf, size_t bufsize, struct media_quality *quality, int nsamples); + int outputs_flush(output_status_cb cb, uint64_t rtptime); @@ -257,6 +307,12 @@ outputs_metadata_free(struct output_metadata *omd); void outputs_authorize(enum output_types type, const char *pin); +int +outputs_quality_subscribe(struct media_quality *quality); + +void +outputs_quality_unsubscribe(struct media_quality *quality); + int outputs_priority(struct output_device *device); From fcc91ecd86c585abbd2e2a8d0b48fe2cc4772bab Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 8 Feb 2019 20:39:11 +0100 Subject: [PATCH 13/86] [player/outputs] Handle multiple quality levels + use rtp_common (WIP!) * Untie Airtunes stuff further from player and non-Airplay outputs * Change raop.c to use rtp_common.c (step 1) * Change heartbeat of player to 100 ticks/sec, since we have untied from Airtunes 352 samples per packet (which equals 126 ticks/sec at 44100) Still a lot to be done in the player, since the rtptime's in it don't are probably broken. --- src/outputs/alsa.c | 37 +- src/outputs/fifo.c | 8 +- src/outputs/pulse.c | 6 +- src/outputs/raop.c | 817 +++++++++++++++++++++----------------------- src/player.c | 284 ++++++++------- src/player.h | 12 +- 6 files changed, 579 insertions(+), 585 deletions(-) diff --git a/src/outputs/alsa.c b/src/outputs/alsa.c index 50ef5d93..87b4cbac 100644 --- a/src/outputs/alsa.c +++ b/src/outputs/alsa.c @@ -38,7 +38,10 @@ #include "player.h" #include "outputs.h" -#define PACKET_SIZE STOB(AIRTUNES_V2_PACKET_SAMPLES) +// Same as Airplay - maybe not optimal? +#define ALSA_SAMPLES_PER_PACKET 352 +#define ALSA_PACKET_SIZE STOB(ALSA_SAMPLES_PER_PACKET, 16, 2) + // 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 @@ -573,15 +576,15 @@ playback_start(struct alsa_session *as, uint64_t pos, uint64_t start_pos) // 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) + as->prebuf_len = (start_pos - pos) / ALSA_SAMPLES_PER_PACKET + 1; + if (as->prebuf_len > (3 * 44100 - offset) / ALSA_SAMPLES_PER_PACKET) { 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); + as->prebuf = malloc(as->prebuf_len * ALSA_PACKET_SIZE); if (!as->prebuf) { DPRINTF(E_LOG, L_LAUDIO, "Out of memory for audio buffer (requested %" PRIu32 " packets)\n", as->prebuf_len); @@ -589,7 +592,7 @@ playback_start(struct alsa_session *as, uint64_t pos, uint64_t start_pos) } as->pos = pos; - as->start_pos = start_pos - AIRTUNES_V2_PACKET_SAMPLES; + as->start_pos = start_pos - ALSA_SAMPLES_PER_PACKET; // Dump PCM config data for E_DBG logging ret = snd_output_buffer_open(&output); @@ -620,32 +623,32 @@ buffer_write(struct alsa_session *as, uint8_t *buf, snd_pcm_sframes_t *avail, in snd_pcm_sframes_t nsamp; snd_pcm_sframes_t ret; - nsamp = AIRTUNES_V2_PACKET_SAMPLES; + nsamp = ALSA_SAMPLES_PER_PACKET; - if (as->prebuf && (prebuffering || !prebuf_empty || *avail < AIRTUNES_V2_PACKET_SAMPLES)) + if (as->prebuf && (prebuffering || !prebuf_empty || *avail < ALSA_SAMPLES_PER_PACKET)) { - pkt = &as->prebuf[as->prebuf_head * PACKET_SIZE]; + pkt = &as->prebuf[as->prebuf_head * ALSA_PACKET_SIZE]; - memcpy(pkt, buf, PACKET_SIZE); + memcpy(pkt, buf, ALSA_PACKET_SIZE); as->prebuf_head = (as->prebuf_head + 1) % as->prebuf_len; - if (prebuffering || *avail < AIRTUNES_V2_PACKET_SAMPLES) + if (prebuffering || *avail < ALSA_SAMPLES_PER_PACKET) 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]; + buf = &as->prebuf[as->prebuf_tail * ALSA_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; + nsamp = npackets * ALSA_SAMPLES_PER_PACKET; while (nsamp > *avail) { npackets -= 1; - nsamp -= AIRTUNES_V2_PACKET_SAMPLES; + nsamp -= ALSA_SAMPLES_PER_PACKET; } as->prebuf_tail = (as->prebuf_tail + npackets) % as->prebuf_len; @@ -685,7 +688,7 @@ sync_check(struct alsa_session *as, uint64_t rtptime, snd_pcm_sframes_t delay, i else npackets = 0; - pb_pos = rtptime - delay - AIRTUNES_V2_PACKET_SAMPLES * npackets; + pb_pos = rtptime - delay - ALSA_SAMPLES_PER_PACKET * npackets; latency = cur_pos - (pb_pos - offset); // If the latency is low or very different from our last measurement, we reset the sync_counter @@ -727,7 +730,7 @@ playback_write(struct alsa_session *as, uint8_t *buf, uint64_t rtptime) prebuffering = (as->pos < as->start_pos); prebuf_empty = (as->prebuf_head == as->prebuf_tail); - as->pos += AIRTUNES_V2_PACKET_SAMPLES; + as->pos += ALSA_SAMPLES_PER_PACKET; if (prebuffering) { @@ -771,7 +774,7 @@ playback_write(struct alsa_session *as, uint8_t *buf, uint64_t rtptime) } // 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); + as->start_pos = as->pos + ALSA_SAMPLES_PER_PACKET * (as->prebuf_len - 1); return; } @@ -799,7 +802,7 @@ playback_pos_get(uint64_t *pos, uint64_t next_pkt) // Make pos the rtptime of the packet containing cur_pos *pos = next_pkt; while (*pos > cur_pos) - *pos -= AIRTUNES_V2_PACKET_SAMPLES; + *pos -= ALSA_SAMPLES_PER_PACKET; } /* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */ diff --git a/src/outputs/fifo.c b/src/outputs/fifo.c index 0aa7c10e..f7ca2775 100644 --- a/src/outputs/fifo.c +++ b/src/outputs/fifo.c @@ -39,13 +39,13 @@ #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[FIFO_PACKET_SIZE]; /* RTP-time of the first sample*/ uint64_t rtptime; @@ -453,7 +453,7 @@ static void fifo_write(uint8_t *buf, uint64_t rtptime) { struct fifo_session *fifo_session = sessions; - size_t length = STOB(AIRTUNES_V2_PACKET_SAMPLES); + size_t length = FIFO_PACKET_SIZE; ssize_t bytes; struct fifo_packet *packet; uint64_t cur_pos; diff --git a/src/outputs/pulse.c b/src/outputs/pulse.c index 86e23cea..6055593e 100644 --- a/src/outputs/pulse.c +++ b/src/outputs/pulse.c @@ -38,6 +38,8 @@ #include "outputs.h" #include "commands.h" +// From Airplay +#define PULSE_SAMPLES_PER_PACKET 352 #define PULSE_MAX_DEVICES 64 #define PULSE_LOG_MAX 10 @@ -602,7 +604,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(2 * ss.rate + PULSE_SAMPLES_PER_PACKET - offset, 16, 2); // 2 second latency ps->attr.maxlength = 2 * ps->attr.tlength; ps->attr.prebuf = (uint32_t)-1; ps->attr.minreq = (uint32_t)-1; @@ -760,7 +762,7 @@ pulse_write(uint8_t *buf, uint64_t rtptime) if (!sessions) return; - length = STOB(AIRTUNES_V2_PACKET_SAMPLES); + length = STOB(PULSE_SAMPLES_PER_PACKET, 16, 2); pa_threaded_mainloop_lock(pulse.mainloop); diff --git a/src/outputs/raop.c b/src/outputs/raop.c index 61b5f0eb..bdf8eaa4 100644 --- a/src/outputs/raop.c +++ b/src/outputs/raop.c @@ -73,22 +73,35 @@ #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 +// AirTunes v2 packet interval in ns */ +// (352 samples/packet * 1e9 ns/s) / 44100 samples/s = 7981859 ns/packet +// #define AIRTUNES_V2_STREAM_PERIOD 7981859 + #ifndef MIN # define MIN(a, b) ((a < b) ? a : b) #endif -#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 ALAC_HEADER_LEN 3 + +#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 +// TODO Should probably not be fixed, but vary with quality +#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) @@ -104,17 +117,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 +166,24 @@ struct raop_extra bool supports_auth_setup; }; +struct raop_master_session +{ + struct evbuffer *evbuf; + + struct rtp_session *rtp_session; + + uint8_t *rawbuf; + size_t rawbuf_size; + int samples_per_packet; + bool encrypt; + + struct raop_master_session *next; +}; + struct raop_session { + struct raop_master_session *master_session; + struct evrtsp_connection *ctrl; enum raop_state state; @@ -252,6 +270,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; @@ -314,16 +333,6 @@ 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; @@ -337,8 +346,11 @@ 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; +/* 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 }; /* ALAC bits writer - big endian * p outgoing buffer pointer @@ -391,30 +403,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); } } @@ -1268,11 +1280,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 = '%'; @@ -1306,7 +1318,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. @@ -1362,8 +1374,9 @@ 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) { - 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); @@ -1384,7 +1397,7 @@ raop_send_req_flush(struct raop_session *rs, uint64_t rtptime, evrtsp_req_cb cb, } /* Restart sequence: last sequence + 1 */ - ret = snprintf(buf, sizeof(buf), "seq=%" PRIu16 ";rtptime=%u", stream_seq + 1, RAOP_RTPTIME(rtptime)); + ret = snprintf(buf, sizeof(buf), "seq=%" PRIu16 ";rtptime=%u", rms->rtp_session->seqnum + 1, 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 +1477,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 +1502,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 + 1, 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"); @@ -1830,9 +1844,11 @@ 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) { @@ -1872,12 +1888,99 @@ raop_status(struct raop_session *rs) player_speaker_status_trigger(); } +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, OUTPUTS_BUFFER_DURATION); + 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); + + 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) +{ + 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); @@ -1905,17 +2008,15 @@ raop_session_free(struct raop_session *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 +2025,11 @@ raop_session_cleanup(struct raop_session *rs) s->next = rs->next; } - raop_session_free(rs); - - /* 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,17 +2037,25 @@ 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) +session_failure_cb(struct evrtsp_request *req, void *arg) +{ + struct raop_session *rs = arg; + + session_failure(rs); +} + +static void +deferredev_cb(int fd, short what, void *arg) { 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); + session_failure(rs); } static void @@ -1980,7 +2073,7 @@ raop_rtsp_close_cb(struct evrtsp_connection *evcon, void *arg) } static struct raop_session * -raop_session_make(struct output_device *rd, int family, output_status_cb cb, bool only_probe) +session_make(struct output_device *rd, int family, output_status_cb cb, bool only_probe) { struct output_session *os; struct raop_session *rs; @@ -2081,7 +2174,7 @@ 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); + rs->deferredev = evtimer_new(evbase_player, deferredev_cb, rs); if (!rs->deferredev) { DPRINTF(E_LOG, L_RAOP, "Out of memory for deferred error handling!\n"); @@ -2155,8 +2248,15 @@ 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; + } + + rs->next = raop_sessions; + raop_sessions = rs; return rs; @@ -2170,16 +2270,9 @@ raop_session_make(struct output_device *rd, int family, output_status_cb cb, boo 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 -------------------------- */ - -/* Metadata handling */ static void raop_cb_metadata(struct evrtsp_request *req, void *arg) { @@ -2208,7 +2301,7 @@ raop_cb_metadata(struct evrtsp_request *req, void *arg) return; error: - raop_session_failure(rs); + session_failure(rs); } static int @@ -2396,7 +2489,7 @@ raop_metadata_startup_send(struct raop_session *rs) ret = raop_metadata_send_internal(rs, rmd, offset, RAOP_MD_DELAY_STARTUP); if (ret < 0) { - raop_session_failure(rs); + session_failure(rs); return; } @@ -2409,7 +2502,7 @@ raop_metadata_startup_send(struct raop_session *rs) ret = raop_metadata_send_internal(rs, rmd, 0, RAOP_MD_DELAY_SWITCH); if (ret < 0) { - raop_session_failure(rs); + session_failure(rs); return; } @@ -2439,7 +2532,7 @@ raop_metadata_send(void *metadata, uint64_t rtptime, uint64_t offset, int startu metadata_tail = rmd; } - for (rs = sessions; rs; rs = next) + for (rs = raop_sessions; rs; rs = next) { next = rs->next; @@ -2454,7 +2547,7 @@ raop_metadata_send(void *metadata, uint64_t rtptime, uint64_t offset, int startu ret = raop_metadata_send_internal(rs, rmd, offset, delay); if (ret < 0) { - raop_session_failure(rs); + session_failure(rs); continue; } } @@ -2599,7 +2692,7 @@ raop_cb_set_volume(struct evrtsp_request *req, void *arg) return; error: - raop_session_failure(rs); + session_failure(rs); } /* Volume in [0 - 100] */ @@ -2620,7 +2713,7 @@ raop_set_volume_one(struct output_device *rd, output_status_cb cb) ret = raop_set_volume_internal(rs, rd->volume, raop_cb_set_volume); if (ret < 0) { - raop_session_failure(rs); + session_failure(rs); return 0; } @@ -2663,7 +2756,7 @@ raop_cb_flush(struct evrtsp_request *req, void *arg) return; error: - raop_session_failure(rs); + session_failure(rs); } static void @@ -2676,7 +2769,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; } @@ -2725,7 +2818,7 @@ raop_cb_pin_start(struct evrtsp_request *req, void *arg) return; error: - raop_session_failure(rs); + session_failure(rs); } @@ -2743,7 +2836,7 @@ raop_flush_timer_cb(int fd, short what, void *arg) DPRINTF(E_DBG, L_RAOP, "Flush timer expired; tearing down RAOP sessions\n"); - for (rs = sessions; rs; rs = rs->next) + for (rs = raop_sessions; rs; rs = rs->next) { if (!(rs->state & RAOP_STATE_F_CONNECTED)) continue; @@ -2757,7 +2850,7 @@ raop_keep_alive_timer_cb(int fd, short what, void *arg) { struct raop_session *rs; - for (rs = sessions; rs; rs = rs->next) + for (rs = raop_sessions; rs; rs = rs->next) { if (!(rs->state & RAOP_STATE_F_CONNECTED)) continue; @@ -2776,7 +2869,7 @@ raop_flush(output_status_cb cb, uint64_t rtptime) int ret; pending = 0; - for (rs = sessions; rs; rs = next) + for (rs = raop_sessions; rs; rs = next) { next = rs->next; @@ -2786,7 +2879,7 @@ raop_flush(output_status_cb cb, uint64_t rtptime) ret = raop_send_req_flush(rs, rtptime, raop_cb_flush, "flush"); if (ret < 0) { - raop_session_failure(rs); + session_failure(rs); continue; } @@ -3050,86 +3143,39 @@ 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) +sync_packet_send(struct raop_session *rs, struct rtp_packet *pkt) { - 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) + switch (rs->sa.ss.ss_family) { - 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"); + case AF_INET: + rs->sa.sin.sin_port = htons(rs->control_port); + len = sizeof(rs->sa.sin); + break; - return; - } + case AF_INET6: + rs->sa.sin6.sin6_port = htons(rs->control_port); + len = sizeof(rs->sa.sin6); + break; - timespec_to_ntp(&ts, &cur_stamp); - } - else - { - cur_pos = next_pkt - 88200; - timespec_to_ntp(init, &cur_stamp); + default: + DPRINTF(E_WARN, L_RAOP, "Unknown family %d\n", rs->sa.ss.ss_family); + return; } - 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)); - } + 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)); } /* Forward */ static void -raop_v2_resend_range(struct raop_session *rs, uint16_t seqnum, uint16_t len); +packets_resend(struct raop_session *rs, uint16_t seqnum, uint16_t len); static void raop_v2_control_cb(int fd, short what, void *arg) @@ -3168,7 +3214,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 +3230,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 +3272,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); @@ -3384,149 +3430,64 @@ raop_v2_control_start(int v6enabled) /* AirTunes v2 streaming */ -static struct raop_v2_packet * -raop_v2_new_packet(void) -{ - struct raop_v2_packet *pkt; - - if (pktbuf_size >= RETRANSMIT_BUFFER_SIZE) - { - 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; - } - } - - return pkt; -} - -static struct raop_v2_packet * -raop_v2_make_packet(uint8_t *rawbuf, uint64_t rtptime) +static int +packet_prepare(struct rtp_packet *pkt, uint8_t *rawbuf, size_t rawbuf_size, bool encrypt) { char ebuf[64]; - struct raop_v2_packet *pkt; gpg_error_t gc_err; - uint32_t rtptime32; - uint16_t seq; - pkt = raop_v2_new_packet(); - if (!pkt) - return NULL; + alac_encode(pkt->payload, rawbuf, rawbuf_size); - memset(pkt, 0, sizeof(struct raop_v2_packet)); + if (!encrypt) + return 0; - 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 */ + // 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; + return -1; } - /* Set IV */ + // 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; + return -1; } - /* 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); + // 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); - - free(pkt); - return NULL; + return -1; } - 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; + return 0; } static int -raop_v2_send_packet(struct raop_session *rs, struct raop_v2_packet *pkt) +packet_send(struct raop_session *rs, struct rtp_packet *pkt) { - uint8_t *data; int ret; if (!rs) return -1; - data = (rs->encrypt) ? pkt->encrypted : pkt->clear; - - ret = send(rs->server_fd, data, AIRTUNES_V2_PKT_LEN, 0); + 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)); - raop_session_failure(rs); + session_failure(rs); return -1; } - else if (ret != AIRTUNES_V2_PKT_LEN) + else if (ret != pkt->data_len) { DPRINTF(E_WARN, L_RAOP, "Partial send (%d) for '%s'\n", ret, rs->devname); return -1; @@ -3535,109 +3496,110 @@ raop_v2_send_packet(struct raop_session *rs, struct raop_v2_packet *pkt) return 0; } +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); +} + + // Forward static void raop_playback_stop(void); -static void -raop_v2_write(uint8_t *buf, uint64_t rtptime) + +static int +frame_send(struct raop_master_session *rms) { - struct raop_v2_packet *pkt; + struct rtp_packet *pkt; + struct rtp_packet *sync_pkt; struct raop_session *rs; struct raop_session *next; + bool sync_send; + int ret; - pkt = raop_v2_make_packet(buf, rtptime); - if (!pkt) + while (evbuffer_get_length(rms->evbuf) >= rms->rawbuf_size) { - raop_playback_stop(); + evbuffer_remove(rms->evbuf, rms->rawbuf, rms->rawbuf_size); - return; + pkt = rtp_packet_next(rms->rtp_session, ALAC_HEADER_LEN + rms->rawbuf_size, rms->samples_per_packet); + + ret = packet_prepare(pkt, rms->rawbuf, rms->rawbuf_size, rms->encrypt); + if (ret < 0) + { + raop_playback_stop(); + return -1; + } + + sync_send = rtp_sync_check(rms->rtp_session, pkt); + if (sync_send) + sync_pkt = rtp_sync_packet_next(rms->rtp_session); + + for (rs = raop_sessions; rs; rs = next) + { + // packet_send() may free rs on failure, so save rs->next now + next = rs->next; + + // A device could have joined after playback_start() was called, so we + // also update state here + if (rs->state == RAOP_STATE_CONNECTED) + rs->state = RAOP_STATE_STREAMING; + + if (rs->master_session != rms || rs->state != RAOP_STATE_STREAMING) + continue; + + if (sync_send) + sync_packet_send(rs, sync_pkt); + + packet_send(rs, pkt); + } + + // Commits packet to retransmit buffer, and prepares the session for the next packet + rtp_packet_commit(rms->rtp_session, pkt); } - if (sync_counter == 126) - { - raop_v2_control_send_sync(rtptime, NULL); - - sync_counter = 1; - } - 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; - - if (rs->state != RAOP_STATE_STREAMING) - continue; - - raop_v2_send_packet(rs, pkt); - } - - return; + return 0; } static void -raop_v2_resend_range(struct raop_session *rs, uint16_t seqnum, uint16_t len) +raop_write(struct output_buffer *obuf) { - struct raop_v2_packet *pktbuf; - int ret; - uint16_t distance; + struct raop_master_session *rms; + int i; - /* Check that seqnum is in the retransmit buffer */ - if ((seqnum > pktbuf_head->seqnum) || (seqnum < pktbuf_tail->seqnum)) + for (rms = raop_master_sessions; rms; rms = rms->next) { - 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) + for (i = 0; obuf->frames[i].buffer; i++) { - DPRINTF(E_LOG, L_RAOP, "Error retransmit packet, aborting retransmission\n"); - return; + if (quality_is_equal(&obuf->frames[i].quality, &rms->rtp_session->quality)) + { +/* DPRINTF(E_LOG, L_RAOP, "raop_write: stream %d, bufsize %zu, samples %d (%d/%d/%d)\n", 0, obuf->frames[0].bufsize, obuf->frames[0].samples, + obuf->frames[0].quality.sample_rate, obuf->frames[0].quality.bits_per_sample, obuf->frames[0].quality.channels); + DPRINTF(E_LOG, L_RAOP, "raop_write: stream %d, bufsize %zu, samples %d (%d/%d/%d)\n", i, obuf->frames[i].bufsize, obuf->frames[i].samples, + obuf->frames[i].quality.sample_rate, obuf->frames[i].quality.bits_per_sample, obuf->frames[i].quality.channels); +*/ + evbuffer_add_buffer_reference(rms->evbuf, obuf->frames[i].evbuf); + frame_send(rms); + } } - - pktbuf = pktbuf->prev; - len--; } - - if (len != 0) - DPRINTF(E_LOG, L_RAOP, "WARNING: len non-zero at end of retransmission\n"); } + static int raop_v2_stream_open(struct raop_session *rs) { @@ -3681,13 +3643,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; @@ -3705,7 +3661,7 @@ raop_startup_cancel(struct raop_session *rs) { if (!rs->session) { - raop_session_failure(rs); + session_failure(rs); return; } @@ -3716,8 +3672,8 @@ raop_startup_cancel(struct raop_session *rs) // 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"); + // Be nice to our peer + session_failure_cb() cleans up old session + raop_send_req_teardown(rs, session_failure_cb, "startup_cancel"); // Try to start a new session raop_device_start(rs->device, rs->status_cb, rs->start_rtptime); @@ -3727,7 +3683,7 @@ raop_startup_cancel(struct raop_session *rs) return; } - raop_send_req_teardown(rs, raop_session_failure_cb, "startup_cancel"); + raop_send_req_teardown(rs, session_failure_cb, "startup_cancel"); } static void @@ -4102,7 +4058,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,7 +4079,7 @@ 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); } @@ -4155,12 +4111,12 @@ raop_cb_shutdown_teardown(struct evrtsp_request *req, void *arg) /* Session shut down, tell our user */ raop_status(rs); - raop_session_cleanup(rs); + session_cleanup(rs); return; error: - raop_session_failure(rs); + session_failure(rs); } @@ -4429,7 +4385,7 @@ raop_cb_verification_setup_step3(struct evrtsp_request *req, void *arg) db_speaker_save(rs->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 +4442,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; @@ -4612,7 +4568,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 +4587,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 +4610,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 +4642,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 +4652,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 +4686,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; @@ -4777,7 +4750,7 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha } 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 *rd, output_status_cb cb, bool only_probe) { struct raop_session *rs; int ret; @@ -4787,11 +4760,9 @@ 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(rd, AF_INET6, cb, only_probe); if (rs) { - rs->start_rtptime = rtptime; - if (rd->auth_key) ret = raop_verification_verify(rs); else if (rd->requires_auth) @@ -4804,16 +4775,14 @@ 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(rd, AF_INET, cb, only_probe); if (!rs) return -1; - rs->start_rtptime = rtptime; - if (rd->auth_key) ret = raop_verification_verify(rs); else if (rd->requires_auth) @@ -4824,7 +4793,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; } @@ -4834,13 +4803,13 @@ 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) { - return raop_device_start_generic(rd, cb, 0, 1); + return raop_device_start_generic(rd, cb, 1); } static int raop_device_start(struct output_device *rd, output_status_cb cb, uint64_t rtptime) { - return raop_device_start_generic(rd, cb, rtptime, 0); + return raop_device_start_generic(rd, cb, 0); } static void @@ -4851,7 +4820,7 @@ raop_device_stop(struct output_session *session) if (rs->state & RAOP_STATE_F_CONNECTED) raop_send_req_teardown(rs, raop_cb_shutdown_teardown, "device_stop"); else - raop_session_cleanup(rs); + session_cleanup(rs); } @@ -4864,23 +4833,32 @@ raop_device_free_extra(struct output_device *device) } static void -raop_playback_start(uint64_t next_pkt, struct timespec *ts) +raop_playback_start(struct timespec *start_time) { + struct raop_master_session *rms; struct raop_session *rs; + struct rtp_packet *sync_pkt; 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; - } + rtp_session_restart(rms->rtp_session, start_time); // Resets sync counter + sync_pkt = rtp_sync_packet_next(rms->rtp_session); - /* Send initial playback sync */ - raop_v2_control_send_sync(next_pkt, ts); + for (rs = raop_sessions; rs; rs = rs->next) + { + if (rs->master_session != rms) + continue; + + if (rs->state == RAOP_STATE_CONNECTED) + rs->state = RAOP_STATE_STREAMING; + + if (sync_pkt && rs->state == RAOP_STATE_STREAMING) + sync_packet_send(rs, sync_pkt); + } + } } static void @@ -4891,7 +4869,7 @@ raop_playback_stop(void) evtimer_del(keep_alive_timer); - for (rs = sessions; rs; rs = rs->next) + for (rs = raop_sessions; rs; rs = rs->next) { ret = raop_send_req_teardown(rs, raop_cb_shutdown_teardown, "playback_stop"); if (ret < 0) @@ -4912,7 +4890,6 @@ raop_init(void) { char ebuf[64]; char *ptr; - char *libname; gpg_error_t gc_err; int v6enabled; int family; @@ -4930,27 +4907,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 +4921,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 +4931,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 +4948,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'; @@ -5064,11 +5025,11 @@ 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(); @@ -5097,9 +5058,9 @@ struct output_definition output_raop = .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_start2 = raop_playback_start, .playback_stop = raop_playback_stop, - .write = raop_v2_write, + .write2 = raop_write, .flush = raop_flush, .status_cb = raop_set_status_cb, .metadata_prepare = raop_metadata_prepare, diff --git a/src/player.c b/src/player.c index db9b6c22..f13222dc 100644 --- a/src/player.c +++ b/src/player.c @@ -111,12 +111,22 @@ // 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 +134,9 @@ // (value is in milliseconds) #define PLAYER_WRITE_BEHIND_MAX 1500 +// TODO fix me +#define TEMP_NEXT_RTPTIME (last_rtptime + pb_session.samples_written + pb_session.samples_per_read) + struct volume_param { int volume; uint64_t spk_id; @@ -172,6 +185,19 @@ union player_arg int intval; }; +struct player_session +{ + uint8_t *buffer; + size_t bufsize; + + uint32_t samples_written; + int samples_per_read; + + struct media_quality quality; +}; + +static struct player_session pb_session; + struct event_base *evbase_player; static int player_exit; @@ -199,15 +225,15 @@ 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; +//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; // Timer resolution static struct timespec timer_res; // Time between two packets -static struct timespec packet_time = { 0, AIRTUNES_V2_STREAM_PERIOD }; +//static struct timespec packet_time = { 0, AIRTUNES_V2_STREAM_PERIOD }; // How many writes we owe the output (when the input is underrunning) static int pb_read_deficit; @@ -242,8 +268,8 @@ 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; +//static uint8_t pb_buffer[STOB(AIRTUNES_V2_PACKET_SAMPLES)]; +//static size_t pb_buffer_offset; // Play history static struct player_history *history; @@ -443,7 +469,7 @@ metadata_trigger(int startup) struct input_metadata metadata; int ret; - ret = input_metadata_get(&metadata, cur_streaming, startup, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES); + ret = input_metadata_get(&metadata, cur_streaming, startup, TEMP_NEXT_RTPTIME); if (ret < 0) return; @@ -652,8 +678,8 @@ source_pause(uint64_t pos) // 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->stream_start = TEMP_NEXT_RTPTIME - ((uint64_t)ret * 44100) / 1000; + cur_streaming->output_start = TEMP_NEXT_RTPTIME; cur_streaming->end = 0; return 0; @@ -676,8 +702,8 @@ source_seek(int seek_ms) 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; + cur_streaming->stream_start = TEMP_NEXT_RTPTIME - ((uint64_t)ret * 44100) / 1000; + cur_streaming->output_start = TEMP_NEXT_RTPTIME; return ret; } @@ -945,7 +971,7 @@ source_switch(int nbytes) DPRINTF(E_DBG, L_PLAYER, "Switching track\n"); - source_close(last_rtptime + AIRTUNES_V2_PACKET_SAMPLES + BTOS(nbytes) - 1); + source_close(TEMP_NEXT_RTPTIME + BTOS(nbytes, pb_session.quality.bits_per_sample, 2) - 1); while ((ps = source_next())) { @@ -960,7 +986,7 @@ source_switch(int nbytes) if (ret < 0) { db_queue_delete_byitemid(ps->item_id); - source_close(last_rtptime + AIRTUNES_V2_PACKET_SAMPLES + BTOS(nbytes) - 1); + source_close(TEMP_NEXT_RTPTIME + BTOS(nbytes, pb_session.quality.bits_per_sample, 2) - 1); continue; } @@ -978,6 +1004,32 @@ source_switch(int nbytes) return 0; } +static void +session_init(struct player_session *session, struct media_quality *quality) +{ + session->samples_written = 0; + session->quality = *quality; + session->samples_per_read = (quality->sample_rate / 1000) * (tick_interval.tv_nsec / 1000000); + session->bufsize = STOB(session->samples_per_read, 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, + session->samples_per_read, session->bufsize); + + if (session->buffer) + session->buffer = realloc(session->buffer, session->bufsize); + else + session->buffer = malloc(session->bufsize); + + CHECK_NULL(L_PLAYER, session->buffer); +} + +static void +session_deinit(struct player_session *session) +{ + free(session->buffer); + memset(session, 0, sizeof(struct player_session)); +} /* ----------------- Main read, write and playback timer event -------------- */ @@ -985,6 +1037,7 @@ source_switch(int nbytes) static int source_read(uint8_t *buf, int len) { + struct media_quality quality; int nbytes; uint32_t item_id; int ret; @@ -1019,6 +1072,11 @@ source_read(uint8_t *buf, int len) { metadata_trigger(0); } + else if (flags & INPUT_FLAG_QUALITY) + { + input_quality_get(&quality); + session_init(&pb_session, &quality); + } // 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) @@ -1031,56 +1089,13 @@ source_read(uint8_t *buf, int len) return nbytes; } -static void -playback_write(void) -{ - int want; - int got; - - source_check(); - - // Make sure playback is still running after source_check() - if (player_state == PLAY_STOPPED) - return; - - pb_read_deficit++; - while (pb_read_deficit) - { - want = sizeof(pb_buffer) - pb_buffer_offset; - got = source_read(pb_buffer + pb_buffer_offset, want); - if (got == want) - { - 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; - return; - } - } -} - static void playback_cb(int fd, short what, void *arg) { - struct timespec next_tick; uint64_t overrun; + int got; + int nsamples; + int i; int ret; // Check if we missed any timer expirations @@ -1125,22 +1140,45 @@ playback_cb(int fd, short what, void *arg) // 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 + pb_read_deficit; i > 0; i--) { - playback_write(); - packet_timer_last = timespec_add(packet_timer_last, packet_time); + source_check(); + + // Make sure playback is still running after source_check() + if (player_state != PLAY_PLAYING) + return; + + got = source_read(pb_session.buffer, pb_session.bufsize); + if (got < 0) + { + DPRINTF(E_LOG, L_PLAYER, "Error reading from source, aborting playback\n"); + playback_abort(); + break; + } + else if (got == 0) + { + pb_read_deficit++; + break; + } + + nsamples = BTOS(got, pb_session.quality.bits_per_sample, pb_session.quality.channels); + outputs_write2(pb_session.buffer, pb_session.bufsize, &pb_session.quality, nsamples); + pb_session.samples_written += nsamples; + + if (got < pb_session.bufsize) + { + DPRINTF(E_DBG, L_PLAYER, "Incomplete read, wanted %zu, got %d\n", pb_session.bufsize, got); + pb_read_deficit++; + } + else if (pb_read_deficit > 0) + pb_read_deficit--; } - 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; - - pb_timer_last = next_tick; + 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(); + } } @@ -1576,15 +1614,9 @@ device_activate_cb(struct output_device *device, struct output_session *session, { 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); + DPRINTF(E_LOG, L_PLAYER, "Could not get current time: %s\n", strerror(errno)); + else + outputs_playback_start2(&ts); } outputs_status_cb(session, device_streaming_cb); @@ -1775,7 +1807,7 @@ playback_abort(void) static void playback_suspend(void) { - player_flush_pending = outputs_flush(device_command_cb, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES); + player_flush_pending = outputs_flush(device_command_cb, TEMP_NEXT_RTPTIME); playback_timer_stop(); @@ -1825,7 +1857,7 @@ get_status(void *arg, int *retval) status->id = cur_streaming->id; status->item_id = cur_streaming->item_id; - pos = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES - cur_streaming->stream_start; + pos = TEMP_NEXT_RTPTIME - cur_streaming->stream_start; status->pos_ms = (pos * 1000) / 44100; status->len_ms = cur_streaming->len_ms; @@ -1909,7 +1941,7 @@ playback_stop(void *arg, int *retval) // 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_command_cb, TEMP_NEXT_RTPTIME); playback_timer_stop(); @@ -1935,35 +1967,29 @@ playback_stop(void *arg, int *retval) static enum command_state playback_start_bh(void *arg, int *retval) { + struct timespec ts; int ret; - ret = clock_gettime_with_res(CLOCK_MONOTONIC, &pb_pos_stamp, &timer_res); - if (ret < 0) - { - DPRINTF(E_LOG, L_PLAYER, "Couldn't get current clock: %s\n", strerror(errno)); - - 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; +// 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_timer_last.tv_sec = pb_pos_stamp.tv_sec; +// pb_timer_last.tv_nsec = pb_pos_stamp.tv_nsec; - pb_buffer_offset = 0; +// pb_buffer_offset = 0; pb_read_deficit = 0; + ret = clock_gettime_with_res(CLOCK_MONOTONIC, &ts, &timer_res); + if (ret < 0) + goto out_fail; + 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_playback_start2(&ts); status_update(PLAY_PLAYING); @@ -1998,7 +2024,7 @@ playback_start_item(void *arg, int *retval) } // Update global playback position - pb_pos = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES - 88200; + pb_pos = TEMP_NEXT_RTPTIME - 88200; if (player_state == PLAY_STOPPED && !queue_item) { @@ -2039,7 +2065,7 @@ playback_start_item(void *arg, int *retval) } } - ret = source_open(ps, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES, seek_ms); + ret = source_open(ps, TEMP_NEXT_RTPTIME, seek_ms); if (ret < 0) { playback_abort(); @@ -2066,7 +2092,7 @@ playback_start_item(void *arg, int *retval) { 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, TEMP_NEXT_RTPTIME); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not start selected %s device '%s'\n", device->type_name, device->name); @@ -2086,7 +2112,7 @@ playback_start_item(void *arg, int *retval) 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, TEMP_NEXT_RTPTIME); if (ret < 0) { DPRINTF(E_DBG, L_PLAYER, "Could not autoselect %s device '%s'\n", device->type_name, device->name); @@ -2202,7 +2228,7 @@ playback_prev_bh(void *arg, int *retval) source_stop(); - ret = source_open(ps, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES, 0); + ret = source_open(ps, TEMP_NEXT_RTPTIME, 0); if (ret < 0) { source_free(ps); @@ -2275,7 +2301,7 @@ playback_next_bh(void *arg, int *retval) source_stop(); - ret = source_open(ps, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES, 0); + ret = source_open(ps, TEMP_NEXT_RTPTIME, 0); if (ret < 0) { source_free(ps); @@ -2388,7 +2414,7 @@ playback_pause(void *arg, int *retval) return COMMAND_END; } - *retval = outputs_flush(device_command_cb, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES); + *retval = outputs_flush(device_command_cb, TEMP_NEXT_RTPTIME); playback_timer_stop(); @@ -2498,7 +2524,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, TEMP_NEXT_RTPTIME); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not start %s device '%s'\n", device->type_name, device->name); @@ -3007,6 +3033,21 @@ player_get_current_pos(uint64_t *pos, struct timespec *ts, int commit) return 0; } +int +player_get_time(struct timespec *ts) +{ + 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; + } + + return 0; +} + int player_get_status(struct player_status *status) { @@ -3444,32 +3485,20 @@ player(void *arg) int player_init(void) { + struct media_quality default_quality = { 44100, 16, 2 }; 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)); + 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 @@ -3485,11 +3514,11 @@ player_init(void) { 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; + 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); + interval = MAX(timer_res.tv_nsec, PLAYER_TICK_INTERVAL * 1000000); tick_interval.tv_nsec = interval; pb_write_deficit_max = (PLAYER_WRITE_BEHIND_MAX * 1000000 / interval); @@ -3532,6 +3561,8 @@ player_init(void) goto evnew_fail; } + session_init(&pb_session, &default_quality); + cmdbase = commands_base_new(evbase_player, NULL); ret = outputs_init(); @@ -3568,6 +3599,7 @@ player_init(void) outputs_deinit(); outputs_fail: commands_base_free(cmdbase); + session_deinit(&pb_session); evnew_fail: event_base_free(evbase_player); evbase_fail: @@ -3600,6 +3632,8 @@ player_deinit(void) player_exit = 1; commands_base_destroy(cmdbase); + session_deinit(&pb_session); + ret = pthread_join(tid_player, NULL); if (ret != 0) { diff --git a/src/player.h b/src/player.h index cfec1800..e8347e4d 100644 --- a/src/player.h +++ b/src/player.h @@ -7,15 +7,6 @@ #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 -// Probably using this value because 44100/352 and 48000/352 has good 32 byte -// alignment, which improves performance of some encoders -#define AIRTUNES_V2_PACKET_SAMPLES 352 - // Maximum number of previously played songs that are remembered #define MAX_HISTORY_COUNT 20 @@ -85,6 +76,9 @@ struct player_history int player_get_current_pos(uint64_t *pos, struct timespec *ts, int commit); +int +player_get_time(struct timespec *ts); + int player_get_status(struct player_status *status); From a924a8dd66e54491742d062071055cda92778d4d Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 8 Feb 2019 21:52:44 +0100 Subject: [PATCH 14/86] [raop] Reorganise code a little --- src/outputs/raop.c | 863 ++++++++++++++++++++++----------------------- 1 file changed, 429 insertions(+), 434 deletions(-) diff --git a/src/outputs/raop.c b/src/outputs/raop.c index bdf8eaa4..dc94e5f8 100644 --- a/src/outputs/raop.c +++ b/src/outputs/raop.c @@ -352,6 +352,16 @@ static struct raop_session *raop_sessions; /* 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 }; +// Forwards +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); + + +/* ------------------------------- MISC HELPERS ----------------------------- */ + /* ALAC bits writer - big endian * p outgoing buffer pointer * val bitfield value @@ -469,10 +479,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) { @@ -811,146 +821,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) { @@ -1304,7 +1176,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 * @@ -2273,6 +2146,143 @@ session_make(struct output_device *rd, int family, output_status_cb cb, bool onl /* ----------------------------- Metadata handling -------------------------- */ +static void +raop_metadata_free(struct raop_metadata *rmd) +{ + evbuffer_free(rmd->metadata); + if (rmd->artwork) + evbuffer_free(rmd->artwork); + free(rmd); +} + +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; +} + static void raop_cb_metadata(struct evrtsp_request *req, void *arg) { @@ -2553,7 +2563,8 @@ raop_metadata_send(void *metadata, uint64_t rtptime, uint64_t offset, int startu } } -/* Volume handling */ + +/* ------------------------------ Volume handling --------------------------- */ static float raop_volume_from_pct(int volume, char *name) @@ -2787,48 +2798,6 @@ 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: - 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) { @@ -2859,47 +2828,176 @@ 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 = raop_sessions; rs; rs = next) + if (!rs) + return -1; + + ret = send(rs->server_fd, pkt->data, pkt->data_len, 0); + if (ret < 0) { - next = rs->next; + DPRINTF(E_LOG, L_RAOP, "Send error for '%s': %s\n", rs->devname, strerror(errno)); - if (rs->state != RAOP_STATE_STREAMING) - continue; + 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; + } - ret = raop_send_req_flush(rs, rtptime, raop_cb_flush, "flush"); + 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 +frame_send(struct raop_master_session *rms) +{ + struct rtp_packet *pkt; + struct rtp_packet *sync_pkt; + struct raop_session *rs; + struct raop_session *next; + bool sync_send; + int ret; + + while (evbuffer_get_length(rms->evbuf) >= rms->rawbuf_size) + { + evbuffer_remove(rms->evbuf, rms->rawbuf, rms->rawbuf_size); + + pkt = rtp_packet_next(rms->rtp_session, ALAC_HEADER_LEN + rms->rawbuf_size, rms->samples_per_packet); + + ret = packet_prepare(pkt, rms->rawbuf, rms->rawbuf_size, rms->encrypt); if (ret < 0) - { - session_failure(rs); + return -1; - continue; + sync_send = rtp_sync_check(rms->rtp_session, pkt); + if (sync_send) + sync_pkt = rtp_sync_packet_next(rms->rtp_session); + + for (rs = raop_sessions; rs; rs = next) + { + // packet_send() may free rs on failure, so save rs->next now + next = rs->next; + + // A device could have joined after playback_start() was called, so we + // also update state here + if (rs->state == RAOP_STATE_CONNECTED) + rs->state = RAOP_STATE_STREAMING; + + if (rs->master_session != rms || rs->state != RAOP_STATE_STREAMING) + continue; + + if (sync_send) + control_packet_send(rs, sync_pkt); + + packet_send(rs, pkt); } - rs->status_cb = cb; - pending++; + // Commits packet to retransmit buffer, and prepares the session for the next packet + rtp_packet_commit(rms->rtp_session, pkt); } - if (pending > 0) - { - evutil_timerclear(&tv); - tv.tv_sec = 10; - evtimer_add(flush_timer, &tv); - } - - return pending; + return 0; } -/* AirTunes v2 time synchronization */ +/* ------------------------------ Time service ------------------------------ */ + static void raop_v2_timing_cb(int fd, short what, void *arg) { @@ -3144,38 +3242,7 @@ raop_v2_timing_start(int v6enabled) } -/* AirTunes v2 playback synchronization */ -static void -sync_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)); -} - -/* Forward */ -static void -packets_resend(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) @@ -3429,177 +3496,72 @@ raop_v2_control_start(int v6enabled) } -/* AirTunes v2 streaming */ -static int -packet_prepare(struct rtp_packet *pkt, uint8_t *rawbuf, size_t rawbuf_size, bool encrypt) +/* ------------------------------ Session startup --------------------------- */ + +static void +raop_startup_cancel(struct raop_session *rs) { - 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) + if (!rs->session) { - 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) -{ - int ret; - - if (!rs) - return -1; - - 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)); - 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; + return; } - return 0; + // 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 + session_failure_cb() cleans up old session + raop_send_req_teardown(rs, 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, session_failure_cb, "startup_cancel"); } static void -packets_resend(struct raop_session *rs, uint16_t seqnum, uint16_t len) +raop_cb_pin_start(struct evrtsp_request *req, void *arg) { - 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); -} - - -// Forward -static void -raop_playback_stop(void); - - -static int -frame_send(struct raop_master_session *rms) -{ - struct rtp_packet *pkt; - struct rtp_packet *sync_pkt; - struct raop_session *rs; - struct raop_session *next; - bool sync_send; + struct raop_session *rs = arg; int ret; - while (evbuffer_get_length(rms->evbuf) >= rms->rawbuf_size) + rs->reqs_in_flight--; + + if (!req) + goto error; + + if (req->response_code != RTSP_OK) { - evbuffer_remove(rms->evbuf, rms->rawbuf, rms->rawbuf_size); + DPRINTF(E_LOG, L_RAOP, "Request for starting PIN verification failed: %d %s\n", req->response_code, req->response_code_line); - pkt = rtp_packet_next(rms->rtp_session, ALAC_HEADER_LEN + rms->rawbuf_size, rms->samples_per_packet); - - ret = packet_prepare(pkt, rms->rawbuf, rms->rawbuf_size, rms->encrypt); - if (ret < 0) - { - raop_playback_stop(); - return -1; - } - - sync_send = rtp_sync_check(rms->rtp_session, pkt); - if (sync_send) - sync_pkt = rtp_sync_packet_next(rms->rtp_session); - - for (rs = raop_sessions; rs; rs = next) - { - // packet_send() may free rs on failure, so save rs->next now - next = rs->next; - - // A device could have joined after playback_start() was called, so we - // also update state here - if (rs->state == RAOP_STATE_CONNECTED) - rs->state = RAOP_STATE_STREAMING; - - if (rs->master_session != rms || rs->state != RAOP_STATE_STREAMING) - continue; - - if (sync_send) - sync_packet_send(rs, sync_pkt); - - packet_send(rs, pkt); - } - - // Commits packet to retransmit buffer, and prepares the session for the next packet - rtp_packet_commit(rms->rtp_session, pkt); + goto error; } - return 0; + 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: + session_failure(rs); } -static void -raop_write(struct output_buffer *obuf) -{ - struct raop_master_session *rms; - int i; - - for (rms = raop_master_sessions; rms; rms = rms->next) - { - for (i = 0; obuf->frames[i].buffer; i++) - { - if (quality_is_equal(&obuf->frames[i].quality, &rms->rtp_session->quality)) - { -/* DPRINTF(E_LOG, L_RAOP, "raop_write: stream %d, bufsize %zu, samples %d (%d/%d/%d)\n", 0, obuf->frames[0].bufsize, obuf->frames[0].samples, - obuf->frames[0].quality.sample_rate, obuf->frames[0].quality.bits_per_sample, obuf->frames[0].quality.channels); - DPRINTF(E_LOG, L_RAOP, "raop_write: stream %d, bufsize %zu, samples %d (%d/%d/%d)\n", i, obuf->frames[i].bufsize, obuf->frames[i].samples, - obuf->frames[i].quality.sample_rate, obuf->frames[i].quality.bits_per_sample, obuf->frames[i].quality.channels); -*/ - evbuffer_add_buffer_reference(rms->evbuf, obuf->frames[i].evbuf); - frame_send(rms); - } - } - } -} - - static int raop_v2_stream_open(struct raop_session *rs) { @@ -3654,38 +3616,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) - { - 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 + session_failure_cb() cleans up old session - raop_send_req_teardown(rs, 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, session_failure_cb, "startup_cancel"); -} - static void raop_cb_startup_volume(struct evrtsp_request *req, void *arg) { @@ -4120,7 +4050,8 @@ raop_cb_shutdown_teardown(struct evrtsp_request *req, void *arg) } -/* 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 @@ -4481,8 +4412,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"] @@ -4749,6 +4682,10 @@ 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, bool only_probe) { @@ -4856,7 +4793,7 @@ raop_playback_start(struct timespec *start_time) rs->state = RAOP_STATE_STREAMING; if (sync_pkt && rs->state == RAOP_STATE_STREAMING) - sync_packet_send(rs, sync_pkt); + control_packet_send(rs, sync_pkt); } } } @@ -4877,6 +4814,64 @@ raop_playback_stop(void) } } +static void +raop_write(struct output_buffer *obuf) +{ + struct raop_master_session *rms; + int i; + + for (rms = raop_master_sessions; rms; rms = rms->next) + { + for (i = 0; obuf->frames[i].buffer; i++) + { + if (!quality_is_equal(&obuf->frames[i].quality, &rms->rtp_session->quality)) + continue; + + evbuffer_add_buffer_reference(rms->evbuf, obuf->frames[i].evbuf); + frame_send(rms); + } + } +} + +static int +raop_flush(output_status_cb cb, uint64_t rtptime) +{ + struct timeval tv; + struct raop_session *rs; + struct raop_session *next; + int pending; + int ret; + + pending = 0; + for (rs = raop_sessions; rs; rs = next) + { + next = rs->next; + + if (rs->state != RAOP_STATE_STREAMING) + continue; + + ret = raop_send_req_flush(rs, rtptime, raop_cb_flush, "flush"); + if (ret < 0) + { + session_failure(rs); + + continue; + } + + rs->status_cb = cb; + pending++; + } + + if (pending > 0) + { + evutil_timerclear(&tv); + tv.tv_sec = 10; + evtimer_add(flush_timer, &tv); + } + + return pending; +} + static void raop_set_status_cb(struct output_session *session, output_status_cb cb) { From 0cb3881621c4fcecdb4db9c815f9cc268b961b7f Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 10 Feb 2019 01:54:52 +0100 Subject: [PATCH 15/86] [player/outputs/raop] Get rid of outputs_playback_start() (still WIP) outputs_playback_start() had the problem that was not consistently invoked: If for instance local audio playback was running and a Airplay device was then activated, the raop's playback_start would never be invoked (and vice versa, of course). Instead, the player now writes the presentation timestamp every time to the output, so it doesn't need to keep track of it from the start. --- src/outputs.c | 68 ++++++------- src/outputs.h | 18 ++-- src/outputs/raop.c | 201 +++++++++++++++++++++++---------------- src/outputs/rtp_common.c | 92 +++++------------- src/outputs/rtp_common.h | 23 +---- src/player.c | 84 ++++++++-------- 6 files changed, 237 insertions(+), 249 deletions(-) diff --git a/src/outputs.c b/src/outputs.c index 873bb3fc..38e0230c 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -140,7 +140,7 @@ encoding_reset(struct media_quality *quality) } static void -buffer_fill(struct output_buffer *obuf, void *buf, size_t bufsize, struct media_quality *quality, int nsamples) +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; @@ -148,6 +148,7 @@ buffer_fill(struct output_buffer *obuf, void *buf, size_t bufsize, struct media_ 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 @@ -217,6 +218,18 @@ outputs_device_start(struct output_device *device, output_status_cb cb, uint64_t return -1; } +int +outputs_device_start2(struct output_device *device, output_status_cb cb) +{ + if (outputs[device->type]->disabled) + return -1; + + if (outputs[device->type]->device_start2) + return outputs[device->type]->device_start2(device, cb); + else + return -1; +} + void outputs_device_stop(struct output_session *session) { @@ -298,36 +311,6 @@ outputs_device_quality_set(struct output_device *device, struct media_quality *q return -1; } -void -outputs_playback_start(uint64_t next_pkt, struct timespec *start_time) -{ - int i; - - for (i = 0; outputs[i]; i++) - { - if (outputs[i]->disabled) - continue; - - if (outputs[i]->playback_start) - outputs[i]->playback_start(next_pkt, start_time); - } -} - -void -outputs_playback_start2(struct timespec *start_time) -{ - int i; - - for (i = 0; outputs[i]; i++) - { - if (outputs[i]->disabled) - continue; - - if (outputs[i]->playback_start2) - outputs[i]->playback_start2(start_time); - } -} - void outputs_playback_stop(void) { @@ -359,11 +342,11 @@ outputs_write(uint8_t *buf, uint64_t rtptime) } void -outputs_write2(void *buf, size_t bufsize, struct media_quality *quality, int nsamples) +outputs_write2(void *buf, size_t bufsize, struct media_quality *quality, int nsamples, struct timespec *pts) { int i; - buffer_fill(&output_buffer, buf, bufsize, quality, nsamples); + buffer_fill(&output_buffer, buf, bufsize, quality, nsamples, pts); for (i = 0; outputs[i]; i++) { @@ -396,6 +379,25 @@ outputs_flush(output_status_cb cb, uint64_t rtptime) return ret; } +int +outputs_flush2(output_status_cb cb) +{ + int ret; + int i; + + ret = 0; + for (i = 0; outputs[i]; i++) + { + if (outputs[i]->disabled) + continue; + + if (outputs[i]->flush2) + ret += outputs[i]->flush2(cb); + } + + return ret; +} + void outputs_status_cb(struct output_session *session, output_status_cb cb) { diff --git a/src/outputs.h b/src/outputs.h index 07b11810..d77547e1 100644 --- a/src/outputs.h +++ b/src/outputs.h @@ -172,6 +172,7 @@ struct output_frame struct output_buffer { uint32_t write_counter; // REMOVE ME? not used for anything + struct timespec *pts; struct output_frame frames[OUTPUTS_MAX_QUALITY_SUBSCRIPTIONS + 1]; } output_buffer; @@ -202,6 +203,7 @@ struct output_definition // 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_start2)(struct output_device *device, output_status_cb cb); // Close a session prepared by device_start void (*device_stop)(struct output_session *session); @@ -223,7 +225,6 @@ struct output_definition // Start/stop playback on devices that were started void (*playback_start)(uint64_t next_pkt, struct timespec *ts); - void (*playback_start2)(struct timespec *start_time); void (*playback_stop)(void); // Write stream data to the output devices @@ -232,6 +233,7 @@ struct output_definition // Flush all sessions, the return must be number of sessions pending the flush int (*flush)(output_status_cb cb, uint64_t rtptime); + int (*flush2)(output_status_cb cb); // Authorize an output with a pin-code (probably coming from the filescanner) void (*authorize)(const char *pin); @@ -249,6 +251,9 @@ struct output_definition int outputs_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime); +int +outputs_device_start2(struct output_device *device, output_status_cb cb); + void outputs_device_stop(struct output_session *session); @@ -268,12 +273,6 @@ outputs_device_volume_to_pct(struct output_device *device, const char *value); int outputs_device_quality_set(struct output_device *device, struct media_quality *quality); -void -outputs_playback_start(uint64_t next_pkt, struct timespec *start_time); - -void -outputs_playback_start2(struct timespec *start_time); - void outputs_playback_stop(void); @@ -281,11 +280,14 @@ void outputs_write(uint8_t *buf, uint64_t rtptime); void -outputs_write2(void *buf, size_t bufsize, struct media_quality *quality, int nsamples); +outputs_write2(void *buf, size_t bufsize, struct media_quality *quality, int nsamples, struct timespec *pts); int outputs_flush(output_status_cb cb, uint64_t rtptime); +int +outputs_flush2(output_status_cb cb); + void outputs_status_cb(struct output_session *session, output_status_cb cb); diff --git a/src/outputs/raop.c b/src/outputs/raop.c index dc94e5f8..4f228980 100644 --- a/src/outputs/raop.c +++ b/src/outputs/raop.c @@ -169,6 +169,7 @@ struct raop_extra struct raop_master_session { struct evbuffer *evbuf; + int evbuf_samples; struct rtp_session *rtp_session; @@ -177,6 +178,11 @@ struct raop_master_session 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; }; @@ -354,7 +360,7 @@ static struct media_quality raop_quality_default = { RAOP_QUALITY_SAMPLE_RATE_DE // Forwards static int -raop_device_start(struct output_device *rd, output_status_cb cb, uint64_t rtptime); +raop_device_start(struct output_device *rd, output_status_cb cb); static void raop_device_stop(struct output_session *session); @@ -1245,7 +1251,7 @@ 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) { struct raop_master_session *rms = rs->master_session; struct evrtsp_request *req; @@ -1269,8 +1275,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", rms->rtp_session->seqnum + 1, rms->rtp_session->pos); + /* 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"); @@ -1375,7 +1381,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", rms->rtp_session->seqnum + 1, rms->rtp_session->pos); + 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"); @@ -1385,6 +1391,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) { @@ -1784,7 +1792,7 @@ master_session_make(struct media_quality *quality, bool encrypt) CHECK_NULL(L_RAOP, rms = calloc(1, sizeof(struct raop_master_session))); - rms->rtp_session = rtp_session_new(quality, RAOP_PACKET_BUFFER_SIZE, 0, OUTPUTS_BUFFER_DURATION); + rms->rtp_session = rtp_session_new(quality, RAOP_PACKET_BUFFER_SIZE, 0); if (!rms->rtp_session) { outputs_quality_unsubscribe(quality); @@ -1795,6 +1803,7 @@ master_session_make(struct media_quality *quality, bool encrypt) 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()); @@ -1810,10 +1819,8 @@ master_session_free(struct raop_master_session *rms) { outputs_quality_unsubscribe(&rms->rtp_session->quality); rtp_session_free(rms->rtp_session); - evbuffer_free(rms->evbuf); free(rms->rawbuf); - free(rms); } @@ -2875,6 +2882,7 @@ packet_prepare(struct rtp_packet *pkt, uint8_t *rawbuf, size_t rawbuf_size, bool static int packet_send(struct raop_session *rs, struct rtp_packet *pkt) { + struct timeval tv; int ret; if (!rs) @@ -2885,7 +2893,12 @@ packet_send(struct raop_session *rs, struct rtp_packet *pkt) { DPRINTF(E_LOG, L_RAOP, "Send error for '%s': %s\n", rs->devname, strerror(errno)); - session_failure(rs); + rs->state = RAOP_STATE_FAILED; + + // Can't free it right away, it would make the ->next in the calling + // master_session and session loops invalid + evutil_timerclear(&tv); + evtimer_add(rs->deferredev, &tv); return -1; } else if (ret != pkt->data_len) @@ -2894,6 +2907,13 @@ packet_send(struct raop_session *rs, struct rtp_packet *pkt) 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; } @@ -2923,6 +2943,8 @@ control_packet_send(struct raop_session *rs, struct rtp_packet *pkt) 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)); + + DPRINTF(E_DBG, L_PLAYER, "SYNC PACKET SENT\n"); } static void @@ -2946,55 +2968,88 @@ packets_resend(struct raop_session *rs, uint16_t seqnum, uint16_t len) } static int -frame_send(struct raop_master_session *rms) +packets_send(struct raop_master_session *rms) { struct rtp_packet *pkt; - struct rtp_packet *sync_pkt; struct raop_session *rs; - struct raop_session *next; - bool sync_send; int ret; - while (evbuffer_get_length(rms->evbuf) >= rms->rawbuf_size) + 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) { - evbuffer_remove(rms->evbuf, rms->rawbuf, rms->rawbuf_size); + if (rs->master_session != rms) + continue; - pkt = rtp_packet_next(rms->rtp_session, ALAC_HEADER_LEN + rms->rawbuf_size, rms->samples_per_packet); - - ret = packet_prepare(pkt, rms->rawbuf, rms->rawbuf_size, rms->encrypt); - if (ret < 0) - return -1; - - sync_send = rtp_sync_check(rms->rtp_session, pkt); - if (sync_send) - sync_pkt = rtp_sync_packet_next(rms->rtp_session); - - for (rs = raop_sessions; rs; rs = next) + // Device just joined + if (rs->state == RAOP_STATE_CONNECTED) { - // packet_send() may free rs on failure, so save rs->next now - next = rs->next; - - // A device could have joined after playback_start() was called, so we - // also update state here - if (rs->state == RAOP_STATE_CONNECTED) - rs->state = RAOP_STATE_STREAMING; - - if (rs->master_session != rms || rs->state != RAOP_STATE_STREAMING) - continue; - - if (sync_send) - control_packet_send(rs, sync_pkt); - + pkt->header[1] = 0xe0; + packet_send(rs, pkt); + rs->state = RAOP_STATE_STREAMING; + } + else if (rs->state == RAOP_STATE_STREAMING) + { + pkt->header[1] = 0x60; packet_send(rs, pkt); } - - // Commits packet to retransmit buffer, and prepares the session for the next packet - rtp_packet_commit(rms->rtp_session, pkt); } + // 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; + 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 do we need this? could we just send the future timestamp? + cur_stamp.ts.tv_sec = pts->tv_sec - OUTPUTS_BUFFER_DURATION; + cur_stamp.ts.tv_nsec = pts->tv_nsec; + // 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) + { + if (rs->master_session != rms) + continue; + + // 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); + } + 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); + } + } +} + /* ------------------------------ Time service ------------------------------ */ @@ -3518,7 +3573,7 @@ raop_startup_cancel(struct raop_session *rs) raop_send_req_teardown(rs, session_failure_cb, "startup_cancel"); // Try to start a new session - raop_device_start(rs->device, rs->status_cb, rs->start_rtptime); + raop_device_start(rs->device, rs->status_cb); // Don't let the failed session make a negative status callback rs->status_cb = NULL; @@ -4744,8 +4799,11 @@ raop_device_probe(struct output_device *rd, output_status_cb cb) } static int -raop_device_start(struct output_device *rd, output_status_cb cb, uint64_t rtptime) +raop_device_start(struct output_device *rd, output_status_cb cb) { + event_del(flush_timer); + evtimer_add(keep_alive_timer, &keep_alive_tv); + return raop_device_start_generic(rd, cb, 0); } @@ -4760,7 +4818,6 @@ raop_device_stop(struct output_session *session) session_cleanup(rs); } - static void raop_device_free_extra(struct output_device *device) { @@ -4769,35 +4826,6 @@ raop_device_free_extra(struct output_device *device) free(re); } -static void -raop_playback_start(struct timespec *start_time) -{ - struct raop_master_session *rms; - struct raop_session *rs; - struct rtp_packet *sync_pkt; - - event_del(flush_timer); - evtimer_add(keep_alive_timer, &keep_alive_tv); - - for (rms = raop_master_sessions; rms; rms = rms->next) - { - rtp_session_restart(rms->rtp_session, start_time); // Resets sync counter - sync_pkt = rtp_sync_packet_next(rms->rtp_session); - - for (rs = raop_sessions; rs; rs = rs->next) - { - if (rs->master_session != rms) - continue; - - if (rs->state == RAOP_STATE_CONNECTED) - rs->state = RAOP_STATE_STREAMING; - - if (sync_pkt && rs->state == RAOP_STATE_STREAMING) - control_packet_send(rs, sync_pkt); - } - } -} - static void raop_playback_stop(void) { @@ -4827,14 +4855,26 @@ raop_write(struct output_buffer *obuf) if (!quality_is_equal(&obuf->frames[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); + evbuffer_add_buffer_reference(rms->evbuf, obuf->frames[i].evbuf); - frame_send(rms); + rms->evbuf_samples += obuf->frames[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); + } } } } static int -raop_flush(output_status_cb cb, uint64_t rtptime) +raop_flush(output_status_cb cb) { struct timeval tv; struct raop_session *rs; @@ -4850,7 +4890,7 @@ raop_flush(output_status_cb cb, uint64_t rtptime) if (rs->state != RAOP_STATE_STREAMING) continue; - ret = raop_send_req_flush(rs, rtptime, raop_cb_flush, "flush"); + ret = raop_send_req_flush(rs, raop_cb_flush, "flush"); if (ret < 0) { session_failure(rs); @@ -5047,16 +5087,15 @@ struct output_definition output_raop = .disabled = 0, .init = raop_init, .deinit = raop_deinit, - .device_start = raop_device_start, + .device_start2 = raop_device_start, .device_stop = raop_device_stop, .device_probe = raop_device_probe, .device_free_extra = raop_device_free_extra, .device_volume_set = raop_set_volume_one, .device_volume_to_pct = raop_volume_to_pct, - .playback_start2 = raop_playback_start, .playback_stop = raop_playback_stop, .write2 = raop_write, - .flush = raop_flush, + .flush2 = raop_flush, .status_cb = raop_set_status_cb, .metadata_prepare = raop_metadata_prepare, .metadata_send = raop_metadata_send, diff --git a/src/outputs/rtp_common.c b/src/outputs/rtp_common.c index 0e8a8014..ecfa3146 100644 --- a/src/outputs/rtp_common.c +++ b/src/outputs/rtp_common.c @@ -45,9 +45,7 @@ #include #include "logger.h" -#include "conffile.h" #include "misc.h" -#include "player.h" #include "rtp_common.h" #define RTP_HEADER_LEN 12 @@ -83,7 +81,7 @@ ntp_to_timespec(struct ntp_timestamp *ns, struct timespec *ts) } struct rtp_session * -rtp_session_new(struct media_quality *quality, int pktbuf_size, int sync_each_nsamples, int buffer_duration) +rtp_session_new(struct media_quality *quality, int pktbuf_size, int sync_each_nsamples) { struct rtp_session *session; @@ -104,10 +102,6 @@ rtp_session_new(struct media_quality *quality, int pktbuf_size, int sync_each_ns else if (sync_each_nsamples == 0) session->sync_each_nsamples = quality->sample_rate; - session->buffer_duration = buffer_duration; - - session->is_virgin = true; - return session; } @@ -125,10 +119,8 @@ rtp_session_free(struct rtp_session *session) } void -rtp_session_restart(struct rtp_session *session, struct timespec *ts) +rtp_session_flush(struct rtp_session *session) { - session->is_virgin = true; - session->start_time = *ts; session->pktbuf_len = 0; session->sync_counter = 0; } @@ -136,7 +128,7 @@ rtp_session_restart(struct rtp_session *session, struct timespec *ts) // 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) +rtp_packet_next(struct rtp_session *session, size_t payload_len, int samples, char type) { struct rtp_packet *pkt; uint16_t seq; @@ -166,7 +158,7 @@ rtp_packet_next(struct rtp_session *session, size_t payload_len, int samples) // RTP Header pkt->header[0] = 0x80; // Version = 2, P, X and CC are 0 - pkt->header[1] = (session->is_virgin) ? 0xe0 : 0x60; // TODO allow other payloads + pkt->header[1] = type; // RTP payload type seq = htobe16(session->seqnum); memcpy(pkt->header + 2, &seq, 2); @@ -177,13 +169,6 @@ rtp_packet_next(struct rtp_session *session, size_t payload_len, int samples) ssrc_id = htobe32(session->ssrc_id); memcpy(pkt->header + 8, &ssrc_id, 4); -/* DPRINTF(E_DBG, L_PLAYER, "RTP PACKET seqnum %u, rtptime %u, payload 0x%x, pktbuf_s %zu\n", - session->seqnum, - session->pos, - pkt->header[1], - session->pktbuf_len - ); -*/ return pkt; } @@ -198,8 +183,7 @@ rtp_packet_commit(struct rtp_session *session, struct rtp_packet *pkt) session->pktbuf_next = (session->pktbuf_next + 1) % session->pktbuf_size; session->seqnum++; session->pos += pkt->samples; - - session->is_virgin = false; + session->sync_counter += pkt->samples; } struct rtp_packet * @@ -226,33 +210,23 @@ rtp_packet_get(struct rtp_session *session, uint16_t seqnum) } bool -rtp_sync_check(struct rtp_session *session, struct rtp_packet *pkt) +rtp_sync_is_time(struct rtp_session *session) { - if (!session->sync_each_nsamples) - { - return false; - } - - if (session->sync_counter > session->sync_each_nsamples) + if (session->sync_each_nsamples && session->sync_counter > session->sync_each_nsamples) { session->sync_counter = 0; return true; } - session->sync_counter += pkt->samples; // TODO Should this move to a sync_commit function? return false; } struct rtp_packet * -rtp_sync_packet_next(struct rtp_session *session) +rtp_sync_packet_next(struct rtp_session *session, struct rtcp_timestamp *cur_stamp, char type) { - struct timespec ts; - struct ntp_timestamp cur_stamp; - uint64_t elapsed_usec; - uint64_t elapsed_samples; + struct ntp_timestamp cur_ts; uint32_t rtptime; uint32_t cur_pos; - int ret; if (!session->sync_packet_next.data) { @@ -260,54 +234,32 @@ rtp_sync_packet_next(struct rtp_session *session) session->sync_packet_next.data_len = RTCP_SYNC_PACKET_LEN; } - memset(session->sync_packet_next.data, 0, session->sync_packet_next.data_len); // TODO remove this and just zero byte 3 instead? - - session->sync_packet_next.data[0] = (session->is_virgin) ? 0x90 : 0x80; + 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; - if (session->is_virgin) - { - session->sync_last_check.pos = session->pos - session->buffer_duration * session->quality.sample_rate; - session->sync_last_check.ts = session->start_time; - timespec_to_ntp(&session->start_time, &cur_stamp); - } - else - { - ret = player_get_time(&ts); - if (ret < 0) - return NULL; + timespec_to_ntp(&cur_stamp->ts, &cur_ts); - elapsed_usec = (ts.tv_sec - session->sync_last_check.ts.tv_sec) * 1000000 + (ts.tv_nsec - session->sync_last_check.ts.tv_nsec) / 1000; - - // How many samples should have been played since last check - elapsed_samples = (elapsed_usec * session->quality.sample_rate) / 1000000; - - session->sync_last_check.pos += elapsed_samples; // TODO should updating sync_last_check move to a commit function? - session->sync_last_check.ts = ts; - timespec_to_ntp(&ts, &cur_stamp); - } - - cur_pos = htobe32(session->sync_last_check.pos); + cur_pos = htobe32(cur_stamp->pos); memcpy(session->sync_packet_next.data + 4, &cur_pos, 4); - cur_stamp.sec = htobe32(cur_stamp.sec); - cur_stamp.frac = htobe32(cur_stamp.frac); - memcpy(session->sync_packet_next.data + 8, &cur_stamp.sec, 4); - memcpy(session->sync_packet_next.data + 12, &cur_stamp.frac, 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 ts:%ld.%ld, next_pkt:%u, cur_pos:%u, payload:0x%x, sync_counter:%d, init:%d\n", - ts.tv_sec, ts.tv_nsec, + 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, - session->sync_last_check.pos, + cur_stamp->pos, session->sync_packet_next.data[0], - session->sync_counter, - session->is_virgin + session->sync_counter ); -*/ + return &session->sync_packet_next; } diff --git a/src/outputs/rtp_common.h b/src/outputs/rtp_common.h index 977412a3..217353e4 100644 --- a/src/outputs/rtp_common.h +++ b/src/outputs/rtp_common.h @@ -37,9 +37,6 @@ struct rtp_session uint32_t pos; uint16_t seqnum; - // True if we haven't started streaming yet - bool is_virgin; - struct media_quality quality; // Packet buffer (ring buffer), used for retransmission @@ -48,35 +45,25 @@ struct rtp_session size_t pktbuf_size; size_t pktbuf_len; - // Time of playback start (given by player) - struct timespec start_time; - - // Number of seconds that we tell the client 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 buffer_duration; - // 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 rtcp_timestamp sync_last_check; }; struct rtp_session * -rtp_session_new(struct media_quality *quality, int pktbuf_size, int sync_each_nsamples, int buffer_duration); +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_restart(struct rtp_session *session, struct timespec *ts); +rtp_session_flush(struct rtp_session *session); struct rtp_packet * -rtp_packet_next(struct rtp_session *session, size_t payload_len, int samples); +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); @@ -85,9 +72,9 @@ struct rtp_packet * rtp_packet_get(struct rtp_session *session, uint16_t seqnum); bool -rtp_sync_check(struct rtp_session *session, struct rtp_packet *pkt); +rtp_sync_is_time(struct rtp_session *session); struct rtp_packet * -rtp_sync_packet_next(struct rtp_session *session); +rtp_sync_packet_next(struct rtp_session *session, struct rtcp_timestamp *cur_stamp, char type); #endif /* !__RTP_COMMON_H__ */ diff --git a/src/player.c b/src/player.c index f13222dc..295514d3 100644 --- a/src/player.c +++ b/src/player.c @@ -134,7 +134,7 @@ // (value is in milliseconds) #define PLAYER_WRITE_BEHIND_MAX 1500 -// TODO fix me +// TODO get rid of me #define TEMP_NEXT_RTPTIME (last_rtptime + pb_session.samples_written + pb_session.samples_per_read) struct volume_param { @@ -194,6 +194,14 @@ struct player_session int samples_per_read; struct media_quality quality; + + // The time the playback session started + struct timespec start_ts; + + // The time the first sample in the buffer should be played by the output. + // It will be equal to: + // pts = start_ts + OUTPUTS_BUFFER_DURATION + ticks_elapsed * player_tick_interval + struct timespec pts; }; static struct player_session pb_session; @@ -225,15 +233,11 @@ 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; @@ -1009,7 +1013,7 @@ session_init(struct player_session *session, struct media_quality *quality) { session->samples_written = 0; session->quality = *quality; - session->samples_per_read = (quality->sample_rate / 1000) * (tick_interval.tv_nsec / 1000000); + session->samples_per_read = (quality->sample_rate / 1000) * (player_tick_interval.tv_nsec / 1000000); session->bufsize = STOB(session->samples_per_read, quality->bits_per_sample, quality->channels); DPRINTF(E_DBG, L_PLAYER, "New session values (q=%d/%d/%d, spr=%d, bufsize=%zu)\n", @@ -1022,6 +1026,10 @@ session_init(struct player_session *session, struct media_quality *quality) session->buffer = malloc(session->bufsize); CHECK_NULL(L_PLAYER, session->buffer); + + clock_gettime_with_res(CLOCK_MONOTONIC, &session->start_ts, &player_timer_res); + session->pts.tv_sec = session->start_ts.tv_sec + OUTPUTS_BUFFER_DURATION; + session->pts.tv_nsec = session->start_ts.tv_nsec; } static void @@ -1092,6 +1100,7 @@ source_read(uint8_t *buf, int len) static void playback_cb(int fd, short what, void *arg) { + struct timespec ts; uint64_t overrun; int got; int nsamples; @@ -1162,16 +1171,25 @@ playback_cb(int fd, short what, void *arg) } nsamples = BTOS(got, pb_session.quality.bits_per_sample, pb_session.quality.channels); - outputs_write2(pb_session.buffer, pb_session.bufsize, &pb_session.quality, nsamples); + outputs_write2(pb_session.buffer, pb_session.bufsize, &pb_session.quality, nsamples, &pb_session.pts); pb_session.samples_written += nsamples; if (got < pb_session.bufsize) { DPRINTF(E_DBG, L_PLAYER, "Incomplete read, wanted %zu, got %d\n", pb_session.bufsize, got); + // How much the number of samples we got corresponds to in time (nanoseconds) + ts.tv_sec = 0; + ts.tv_nsec = 1000000000L * nsamples / pb_session.quality.sample_rate; + pb_session.pts = timespec_add(pb_session.pts, ts); pb_read_deficit++; } - else if (pb_read_deficit > 0) - pb_read_deficit--; + 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); + if (pb_read_deficit > 0) + pb_read_deficit--; + } } if (pb_read_deficit > pb_read_deficit_max) @@ -1568,7 +1586,6 @@ device_lost_cb(struct output_device *device, struct output_session *session, enu 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; @@ -1610,15 +1627,6 @@ device_activate_cb(struct output_device *device, struct output_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)); - else - outputs_playback_start2(&ts); - } - outputs_status_cb(session, device_streaming_cb); out: @@ -1742,8 +1750,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); @@ -1807,7 +1815,7 @@ playback_abort(void) static void playback_suspend(void) { - player_flush_pending = outputs_flush(device_command_cb, TEMP_NEXT_RTPTIME); + player_flush_pending = outputs_flush2(device_command_cb); playback_timer_stop(); @@ -1941,7 +1949,7 @@ playback_stop(void *arg, int *retval) // 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, TEMP_NEXT_RTPTIME); + *retval = outputs_flush2(device_command_cb); playback_timer_stop(); @@ -1981,7 +1989,7 @@ playback_start_bh(void *arg, int *retval) // pb_buffer_offset = 0; pb_read_deficit = 0; - ret = clock_gettime_with_res(CLOCK_MONOTONIC, &ts, &timer_res); + ret = clock_gettime_with_res(CLOCK_MONOTONIC, &ts, &player_timer_res); if (ret < 0) goto out_fail; @@ -1989,8 +1997,6 @@ playback_start_bh(void *arg, int *retval) if (ret < 0) goto out_fail; - outputs_playback_start2(&ts); - status_update(PLAY_PLAYING); *retval = 0; @@ -2092,7 +2098,7 @@ playback_start_item(void *arg, int *retval) { if (device->selected && !device->session) { - ret = outputs_device_start(device, device_restart_cb, TEMP_NEXT_RTPTIME); + ret = outputs_device_start2(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); @@ -2414,7 +2420,7 @@ playback_pause(void *arg, int *retval) return COMMAND_END; } - *retval = outputs_flush(device_command_cb, TEMP_NEXT_RTPTIME); + *retval = outputs_flush2(device_command_cb); playback_timer_stop(); @@ -2524,7 +2530,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, TEMP_NEXT_RTPTIME); + ret = outputs_device_start2(device, device_activate_cb); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not start %s device '%s'\n", device->type_name, device->name); @@ -2996,7 +3002,7 @@ 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); + ret = clock_gettime_with_res(CLOCK_MONOTONIC, ts, &player_timer_res); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Couldn't get clock: %s\n", strerror(errno)); @@ -3038,7 +3044,7 @@ player_get_time(struct timespec *ts) { int ret; - ret = clock_gettime_with_res(CLOCK_MONOTONIC, ts, &timer_res); + ret = clock_gettime_with_res(CLOCK_MONOTONIC, ts, &player_timer_res); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Couldn't get clock: %s\n", strerror(errno)); @@ -3503,7 +3509,7 @@ player_init(void) // 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"); @@ -3512,14 +3518,14 @@ player_init(void) 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); + DPRINTF(E_INFO, L_PLAYER, "High resolution clock not enabled on this system (res is %ld)\n", player_timer_res.tv_nsec); - timer_res.tv_nsec = 10 * PLAYER_TICK_INTERVAL * 1000000; + player_timer_res.tv_nsec = 10 * PLAYER_TICK_INTERVAL * 1000000; } // Set the tick interval for the playback timer - interval = MAX(timer_res.tv_nsec, PLAYER_TICK_INTERVAL * 1000000); - 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); From 94dfef6e78e181b21dd17ca74066dbb25d8a976e Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 10 Feb 2019 23:16:46 +0100 Subject: [PATCH 16/86] [input] Return quality flag even if read size is zero First input_read() has bufsize 0, because we don't know quality yet --- src/input.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/input.c b/src/input.c index 9bdea003..cda5f13b 100644 --- a/src/input.c +++ b/src/input.c @@ -323,7 +323,7 @@ input_read(void *data, size_t size, short *flags) // 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) + if (marker && marker->pos <= input_buffer.bytes_read + size) { *flags = marker->flags; if (*flags & INPUT_FLAG_QUALITY) From cee740ae51c0f02a7dbbacad54f4d49727960506 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 10 Feb 2019 23:18:26 +0100 Subject: [PATCH 17/86] [misc] Move MIN() macro from raop.c to misc.c --- src/misc.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/misc.h b/src/misc.h index cd88ea05..2921cfa4 100644 --- a/src/misc.h +++ b/src/misc.h @@ -17,6 +17,10 @@ #define ARRAY_SIZE(x) ((unsigned int)(sizeof(x) / sizeof((x)[0]))) +#ifndef MIN +# define MIN(a, b) ((a < b) ? a : b) +#endif + // Remember to adjust quality_is_equal() if adding elements struct media_quality { int sample_rate; From 14a6d318f0090cd99b4a55d4ca47ac36e90e5517 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 10 Feb 2019 23:20:12 +0100 Subject: [PATCH 18/86] [outputs] Changes to interface of outputs module * Drop output_sessions, was just a pointer to the actual session anyway * Drop the old write, flush and stop prototypes * Some minor changes/renaming Purpose of this is also to fix a race condition in player.c where it could try to start two sessions on the same speaker. This could happen because outputs_device_start() in line 2093 is conditional on device->session which however is false while a device is starting up. --- src/outputs.c | 117 ++++++++++++++++++-------------------------------- src/outputs.h | 55 ++++++++---------------- 2 files changed, 59 insertions(+), 113 deletions(-) diff --git a/src/outputs.c b/src/outputs.c index 38e0230c..c0eb861f 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -148,7 +148,7 @@ buffer_fill(struct output_buffer *obuf, void *buf, size_t bufsize, struct media_ int n; obuf->write_counter++; - obuf->pts = pts; + 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 @@ -207,49 +207,48 @@ buffer_drain(struct output_buffer *obuf) /* ----------------------------------- API ---------------------------------- */ int -outputs_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime) +outputs_device_start(struct output_device *device, output_status_cb cb) { - if (outputs[device->type]->disabled) + if (outputs[device->type]->disabled || !outputs[device->type]->device_start) return -1; - if (outputs[device->type]->device_start) - return outputs[device->type]->device_start(device, cb, rtptime); - else - 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, cb); } int -outputs_device_start2(struct output_device *device, output_status_cb cb) +outputs_device_stop(struct output_device *device, output_status_cb cb) { - if (outputs[device->type]->disabled) + if (outputs[device->type]->disabled || !outputs[device->type]->device_stop) return -1; - if (outputs[device->type]->device_start2) - return outputs[device->type]->device_start2(device, cb); - else - 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; + } -void -outputs_device_stop(struct output_session *session) -{ - if (outputs[session->type]->disabled) - return; - - if (outputs[session->type]->device_stop) - outputs[session->type]->device_stop(session); + return outputs[device->type]->device_stop(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 - return -1; + 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, cb); } void @@ -311,6 +310,16 @@ outputs_device_quality_set(struct output_device *device, struct media_quality *q return -1; } +void +outputs_device_set_cb(struct output_device *device, output_status_cb cb) +{ + if (outputs[device->type]->disabled) + return; + + if (outputs[device->type]->device_set_cb) + outputs[device->type]->device_set_cb(device, cb); +} + void outputs_playback_stop(void) { @@ -327,22 +336,7 @@ outputs_playback_stop(void) } void -outputs_write(uint8_t *buf, uint64_t rtptime) -{ - int i; - - for (i = 0; outputs[i]; i++) - { - if (outputs[i]->disabled) - continue; - - if (outputs[i]->write) - outputs[i]->write(buf, rtptime); - } -} - -void -outputs_write2(void *buf, size_t bufsize, struct media_quality *quality, int nsamples, struct timespec *pts) +outputs_write(void *buf, size_t bufsize, struct media_quality *quality, int nsamples, struct timespec *pts) { int i; @@ -353,15 +347,15 @@ outputs_write2(void *buf, size_t bufsize, struct media_quality *quality, int nsa if (outputs[i]->disabled) continue; - if (outputs[i]->write2) - outputs[i]->write2(&output_buffer); + if (outputs[i]->write) + outputs[i]->write(&output_buffer); } buffer_drain(&output_buffer); } int -outputs_flush(output_status_cb cb, uint64_t rtptime) +outputs_flush(output_status_cb cb) { int ret; int i; @@ -373,41 +367,12 @@ outputs_flush(output_status_cb cb, uint64_t rtptime) continue; if (outputs[i]->flush) - ret += outputs[i]->flush(cb, rtptime); + ret += outputs[i]->flush(cb); } return ret; } -int -outputs_flush2(output_status_cb cb) -{ - int ret; - int i; - - ret = 0; - for (i = 0; outputs[i]; i++) - { - if (outputs[i]->disabled) - continue; - - if (outputs[i]->flush2) - ret += outputs[i]->flush2(cb); - } - - return ret; -} - -void -outputs_status_cb(struct output_session *session, output_status_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) { diff --git a/src/outputs.h b/src/outputs.h index d77547e1..c6763b49 100644 --- a/src/outputs.h +++ b/src/outputs.h @@ -140,18 +140,11 @@ struct output_device // 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 { @@ -172,12 +165,12 @@ struct output_frame struct output_buffer { uint32_t write_counter; // REMOVE ME? not used for anything - struct timespec *pts; + struct timespec pts; struct output_frame frames[OUTPUTS_MAX_QUALITY_SUBSCRIPTIONS + 1]; } output_buffer; -typedef void (*output_status_cb)(struct output_device *device, struct output_session *session, enum output_device_state status); +typedef void (*output_status_cb)(struct output_device *device, enum output_device_state status); struct output_definition { @@ -202,11 +195,10 @@ 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_start2)(struct output_device *device, output_status_cb cb); + int (*device_start)(struct output_device *device, output_status_cb cb); - // 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, output_status_cb cb); // Test the connection to a device and call back int (*device_probe)(struct output_device *device, output_status_cb cb); @@ -223,24 +215,22 @@ struct output_definition // Request a change of quality from the device int (*quality_set)(struct output_device *device, struct media_quality *quality); + // Change the call back associated with a device + void (*device_set_cb)(struct output_device *device, output_status_cb cb); + // Start/stop playback on devices that were started void (*playback_start)(uint64_t next_pkt, struct timespec *ts); void (*playback_stop)(void); // Write stream data to the output devices - void (*write)(uint8_t *buf, uint64_t rtptime); - void (*write2)(struct output_buffer *buffer); + void (*write)(struct output_buffer *buffer); // Flush all sessions, the return must be number of sessions pending the flush - int (*flush)(output_status_cb cb, uint64_t rtptime); - int (*flush2)(output_status_cb cb); + int (*flush)(output_status_cb cb); // 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); - // Metadata void *(*metadata_prepare)(int id); void (*metadata_send)(void *metadata, uint64_t rtptime, uint64_t offset, int startup); @@ -249,13 +239,10 @@ struct output_definition }; int -outputs_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime); +outputs_device_start(struct output_device *device, output_status_cb cb); int -outputs_device_start2(struct output_device *device, output_status_cb cb); - -void -outputs_device_stop(struct output_session *session); +outputs_device_stop(struct output_device *device, output_status_cb cb); int outputs_device_probe(struct output_device *device, output_status_cb cb); @@ -273,23 +260,17 @@ outputs_device_volume_to_pct(struct output_device *device, const char *value); int outputs_device_quality_set(struct output_device *device, struct media_quality *quality); +void +outputs_device_set_cb(struct output_device *device, output_status_cb cb); + void outputs_playback_stop(void); void -outputs_write(uint8_t *buf, uint64_t rtptime); - -void -outputs_write2(void *buf, size_t bufsize, struct media_quality *quality, int nsamples, struct timespec *pts); +outputs_write(void *buf, size_t bufsize, struct media_quality *quality, int nsamples, struct timespec *pts); int -outputs_flush(output_status_cb cb, uint64_t rtptime); - -int -outputs_flush2(output_status_cb cb); - -void -outputs_status_cb(struct output_session *session, output_status_cb cb); +outputs_flush(output_status_cb cb); struct output_metadata * outputs_metadata_prepare(int id); From e99f20992e586deff2eb9dc93c71cec107435d0b Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 10 Feb 2019 23:27:29 +0100 Subject: [PATCH 19/86] [player/outputs] Implement changed output interfaces in most backends Still missing cast, alsa and pulseaudio, but these can so far just be disabled with configure. Otherwise still mostly untested. --- src/httpd_streaming.c | 248 ++++++++++++++++---------------- src/httpd_streaming.h | 3 +- src/outputs/alsa.c | 24 +--- src/outputs/cast.c | 24 +--- src/outputs/dummy.c | 60 +++----- src/outputs/fifo.c | 133 +++++++++--------- src/outputs/pulse.c | 26 +--- src/outputs/raop.c | 297 +++++++++++++++++++-------------------- src/outputs/rtp_common.c | 4 +- src/outputs/streaming.c | 1 - src/player.c | 131 ++++++++--------- src/player.h | 7 - 12 files changed, 429 insertions(+), 529 deletions(-) diff --git a/src/httpd_streaming.c b/src/httpd_streaming.c index daccb01c..c7d50e61 100644 --- a/src/httpd_streaming.c +++ b/src/httpd_streaming.c @@ -44,10 +44,8 @@ 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 -// How many samples we store in the buffer used for transmitting from player to httpd thread -#define STREAMING_RAWBUF_SAMPLES 352 -// Buffer size -#define STREAMING_RAWBUF_SIZE (STOB(STREAMING_RAWBUF_SAMPLES, 16, 2)) +// How many bytes we try to read at a time from the httpd pipe +#define STREAMING_READ_SIZE STOB(352, 16, 2) // Linked list of mp3 streaming requests struct streaming_session { @@ -56,23 +54,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 @@ -116,12 +115,77 @@ streaming_fail_cb(struct evhttp_connection *evcon, void *arg) } } +static void +streaming_meta_cb(evutil_socket_t fd, short event, void *arg) +{ + struct media_quality quality; + struct decode_ctx *decode_ctx; + int ret; + + ret = read(fd, &quality, sizeof(struct media_quality)); + if (ret != sizeof(struct media_quality)) + goto error; + + streaming_quality = quality; + + decode_ctx = NULL; + if (quality.sample_rate == 44100 && quality.bits_per_sample == 16) + decode_ctx = transcode_decode_setup_raw(XCODE_PCM16_44100); + else if (quality.sample_rate == 44100 && quality.bits_per_sample == 24) + decode_ctx = transcode_decode_setup_raw(XCODE_PCM24_44100); + else if (quality.sample_rate == 48000 && quality.bits_per_sample == 16) + decode_ctx = transcode_decode_setup_raw(XCODE_PCM16_48000); + else if (quality.sample_rate == 48000 && quality.bits_per_sample == 24) + decode_ctx = transcode_decode_setup_raw(XCODE_PCM24_48000); + + if (!decode_ctx) + goto error; + + 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"); + streaming_not_supported = 1; + return; + } + + streaming_not_supported = 0; + + error: + DPRINTF(E_LOG, L_STREAMING, "Unknown or unsupported quality of input data, cannot MP3 encode\n"); + transcode_encode_cleanup(&streaming_encode_ctx); + streaming_not_supported = 1; +} + +static int +encode_buffer(uint8_t *buffer, size_t size) +{ + transcode_frame *frame; + int samples; + int ret; + + samples = BTOS(size, streaming_quality.bits_per_sample, streaming_quality.channels); + + frame = transcode_frame_new(buffer, size, samples, streaming_quality.sample_rate, streaming_quality.bits_per_sample); + 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; @@ -129,24 +193,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(streaming_rawbuf, STREAMING_RAWBUF_SIZE, STREAMING_RAWBUF_SAMPLES, 44100, 16); - 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 @@ -157,16 +213,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(); @@ -181,6 +239,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); } @@ -193,14 +252,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->frames[0].quality, &streaming_quality)) + { + ret = write(streaming_meta[1], &obuf->frames[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->frames[0].buffer, obuf->frames[0].bufsize); if (ret < 0) { if (errno == EAGAIN) @@ -221,9 +290,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; @@ -286,26 +355,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(XCODE_PCM16_44100); - 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); @@ -320,7 +371,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 @@ -328,77 +395,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, 16, 2); - while (remaining > STREAMING_RAWBUF_SIZE) - { - frame = transcode_frame_new(streaming_rawbuf, STREAMING_RAWBUF_SIZE, STREAMING_RAWBUF_SAMPLES, 44100, 16); - 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; } @@ -409,9 +421,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 @@ -430,8 +439,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/outputs/alsa.c b/src/outputs/alsa.c index 87b4cbac..9294fcff 100644 --- a/src/outputs/alsa.c +++ b/src/outputs/alsa.c @@ -105,9 +105,7 @@ struct alsa_session 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; struct alsa_session *next; @@ -147,7 +145,6 @@ alsa_session_free(struct alsa_session *as) prebuf_free(as); - free(as->output_session); free(as); } @@ -169,6 +166,8 @@ alsa_session_cleanup(struct alsa_session *as) s->next = as->next; } + as->device->session = NULL; + alsa_session_free(as); } @@ -177,21 +176,7 @@ alsa_session_make(struct output_device *device, output_status_cb cb) { struct alsa_session *as; - as = calloc(1, sizeof(struct alsa_session)); - if (!as) - { - DPRINTF(E_LOG, L_LAUDIO, "Out of memory for ALSA session (as)\n"); - return NULL; - } - - 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; + CHECK_NULL(L_LAUDIO, as = calloc(1, sizeof(struct alsa_session))); as->deferredev = evtimer_new(evbase_player, defer_cb, as); if (!as->deferredev) @@ -210,6 +195,9 @@ alsa_session_make(struct output_device *device, output_status_cb cb) as->next = sessions; sessions = as; + + device->session = as; + return as; failure_cleanup: diff --git a/src/outputs/cast.c b/src/outputs/cast.c index f777cac9..7ddb7b65 100644 --- a/src/outputs/cast.c +++ b/src/outputs/cast.c @@ -177,9 +177,7 @@ struct cast_session 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; struct cast_session *next; @@ -1327,7 +1325,6 @@ cast_device_cb(const char *name, const char *type, const char *domain, const cha static struct cast_session * cast_session_make(struct output_device *device, int family, output_status_cb cb) { - struct output_session *os; struct cast_session *cs; const char *proto; const char *err; @@ -1359,25 +1356,8 @@ 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; @@ -1440,6 +1420,8 @@ cast_session_make(struct output_device *device, int family, output_status_cb cb) cs->next = sessions; sessions = cs; + device->session = cs; + proto = gnutls_protocol_get_name(gnutls_protocol_get_version(cs->tls_session)); DPRINTF(E_INFO, L_CAST, "Connection to '%s' established using %s\n", cs->devname, proto); diff --git a/src/outputs/dummy.c b/src/outputs/dummy.c index 3083ead4..78f24553 100644 --- a/src/outputs/dummy.c +++ b/src/outputs/dummy.c @@ -47,9 +47,7 @@ struct dummy_session 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; }; @@ -72,7 +70,6 @@ dummy_session_free(struct dummy_session *ds) event_free(ds->deferredev); - free(ds->output_session); free(ds); } @@ -82,27 +79,20 @@ dummy_session_cleanup(struct dummy_session *ds) // Normally some here code to remove from linked list - here we just say: sessions = NULL; + ds->device->session = NULL; + dummy_session_free(ds); } static struct dummy_session * dummy_session_make(struct output_device *device, output_status_cb cb) { - 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; - } - 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; } @@ -110,21 +100,18 @@ dummy_session_make(struct output_device *device, output_status_cb cb) 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; sessions = ds; + device->session = ds; + return ds; } @@ -139,7 +126,7 @@ 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); + ds->defer_cb(ds->device, ds->state); if (ds->state == OUTPUT_STATE_STOPPED) dummy_session_cleanup(ds); @@ -157,7 +144,7 @@ dummy_status(struct dummy_session *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, output_status_cb cb) { struct dummy_session *ds; @@ -170,13 +157,15 @@ 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) +static int +dummy_device_stop(struct output_device *device, output_status_cb cb) { - struct dummy_session *ds = session->session; + struct dummy_session *ds = device->session; ds->state = OUTPUT_STATE_STOPPED; dummy_status(ds); + + return 0; } static int @@ -199,13 +188,11 @@ 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) { - struct dummy_session *ds; + struct dummy_session *ds = device->session; - if (!device->session || !device->session->session) + if (!ds) return 0; - ds = device->session->session; - ds->status_cb = cb; dummy_status(ds); @@ -213,15 +200,11 @@ dummy_device_volume_set(struct output_device *device, output_status_cb cb) } static void -dummy_playback_start(uint64_t next_pkt, struct timespec *ts) +dummy_device_set_cb(struct output_device *device, output_status_cb cb) { - struct dummy_session *ds = sessions; + struct dummy_session *ds = device->session; - if (!sessions) - return; - - ds->state = OUTPUT_STATE_STREAMING; - dummy_status(ds); + ds->status_cb = cb; } static void @@ -236,14 +219,6 @@ dummy_playback_stop(void) dummy_status(ds); } -static void -dummy_set_status_cb(struct output_session *session, output_status_cb cb) -{ - struct dummy_session *ds = session->session; - - ds->status_cb = cb; -} - static int dummy_init(void) { @@ -298,7 +273,6 @@ struct output_definition output_dummy = .device_stop = dummy_device_stop, .device_probe = dummy_device_probe, .device_volume_set = dummy_device_volume_set, - .playback_start = dummy_playback_start, + .device_set_cb = dummy_device_set_cb, .playback_stop = dummy_playback_stop, - .status_cb = dummy_set_status_cb, }; diff --git a/src/outputs/fifo.c b/src/outputs/fifo.c index f7ca2775..f0660772 100644 --- a/src/outputs/fifo.c +++ b/src/outputs/fifo.c @@ -45,10 +45,11 @@ struct fifo_packet { /* pcm data */ - uint8_t samples[FIFO_PACKET_SIZE]; + 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 +60,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() @@ -94,9 +98,7 @@ struct fifo_session 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; }; @@ -242,7 +244,6 @@ fifo_session_free(struct fifo_session *fifo_session) event_free(fifo_session->deferredev); - free(fifo_session->output_session); free(fifo_session); free_buffer(); } @@ -253,43 +254,26 @@ fifo_session_cleanup(struct fifo_session *fifo_session) // Normally some here code to remove from linked list - here we just say: sessions = NULL; + fifo_session->device->session = NULL; + fifo_session_free(fifo_session); } static struct fifo_session * fifo_session_make(struct output_device *device, output_status_cb cb) { - 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; - } - - 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; - } + CHECK_NULL(L_FIFO, fifo_session = calloc(1, sizeof(struct fifo_session))); 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; @@ -301,6 +285,8 @@ fifo_session_make(struct output_device *device, output_status_cb cb) sessions = fifo_session; + device->session = fifo_session; + return fifo_session; } @@ -315,7 +301,7 @@ 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); + ds->defer_cb(ds->device, ds->state); if (ds->state == OUTPUT_STATE_STOPPED) fifo_session_cleanup(ds); @@ -333,11 +319,15 @@ fifo_status(struct fifo_session *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, output_status_cb cb) { struct fifo_session *fifo_session; int ret; + ret = outputs_quality_subscribe(&fifo_quality); + if (ret < 0) + return -1; + fifo_session = fifo_session_make(device, cb); if (!fifo_session) return -1; @@ -351,16 +341,22 @@ 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, output_status_cb cb) { - struct fifo_session *fifo_session = output_session->session; + struct fifo_session *fifo_session = device->session; + + outputs_quality_unsubscribe(&fifo_quality); + + fifo_session->status_cb = cb; fifo_close(fifo_session); free_buffer(); fifo_session->state = OUTPUT_STATE_STOPPED; fifo_status(fifo_session); + + return 0; } static int @@ -393,13 +389,11 @@ 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) { - 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_status(fifo_session); @@ -407,15 +401,11 @@ fifo_device_volume_set(struct output_device *device, output_status_cb cb) } static void -fifo_playback_start(uint64_t next_pkt, struct timespec *ts) +fifo_device_set_cb(struct output_device *device, output_status_cb cb) { - struct fifo_session *fifo_session = sessions; + struct fifo_session *fifo_session = device->session; - if (!fifo_session) - return; - - fifo_session->state = OUTPUT_STATE_STREAMING; - fifo_status(fifo_session); + fifo_session->status_cb = cb; } static void @@ -433,7 +423,7 @@ fifo_playback_stop(void) } static int -fifo_flush(output_status_cb cb, uint64_t rtptime) +fifo_flush(output_status_cb cb) { struct fifo_session *fifo_session = sessions; @@ -450,45 +440,59 @@ fifo_flush(output_status_cb cb, uint64_t rtptime) } static void -fifo_write(uint8_t *buf, uint64_t rtptime) +fifo_write(struct output_buffer *obuf) { struct fifo_session *fifo_session = sessions; - size_t length = FIFO_PACKET_SIZE; - ssize_t bytes; struct fifo_packet *packet; - uint64_t cur_pos; struct timespec now; - int ret; + ssize_t bytes; + int i; 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; + for (i = 0; obuf->frames[i].buffer; i++) + { + if (quality_is_equal(&fifo_quality, &obuf->frames[i].quality)) + break; + } + + if (!obuf->frames[i].buffer) + { + DPRINTF(E_LOG, L_FIFO, "Bug! Did not get audio in quality required\n"); + return; + } + + fifo_session->state = OUTPUT_STATE_STREAMING; + + CHECK_NULL(L_FIFO, packet = calloc(1, sizeof(struct fifo_packet))); + CHECK_NULL(L_FIFO, packet->samples = malloc(obuf->frames[i].bufsize)); + + memcpy(packet->samples, obuf->frames[i].buffer, obuf->frames[i].bufsize); + packet->samples_size = obuf->frames[i].bufsize; + packet->pts = obuf->pts; + 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 +515,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) { @@ -578,9 +574,8 @@ struct output_definition output_fifo = .device_stop = fifo_device_stop, .device_probe = fifo_device_probe, .device_volume_set = fifo_device_volume_set, - .playback_start = fifo_playback_start, + .device_set_cb = fifo_device_set_cb, .playback_stop = fifo_playback_stop, .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 6055593e..0a962105 100644 --- a/src/outputs/pulse.c +++ b/src/outputs/pulse.c @@ -70,9 +70,7 @@ struct pulse_session 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; @@ -141,34 +139,18 @@ pulse_session_cleanup(struct pulse_session *ps) p->next = ps->next; } + ps->device->session = NULL; + pulse_session_free(ps); } static struct pulse_session * pulse_session_make(struct output_device *device, output_status_cb cb) { - struct output_session *os; struct pulse_session *ps; - os = calloc(1, sizeof(struct output_session)); - if (!os) - { - DPRINTF(E_LOG, L_LAUDIO, "Out of memory (os)\n"); - return NULL; - } + CHECK_NULL(L_LAUDIO, ps = calloc(1, sizeof(struct pulse_session))); - ps = calloc(1, sizeof(struct pulse_session)); - if (!ps) - { - DPRINTF(E_LOG, L_LAUDIO, "Out of memory (ps)\n"); - free(os); - return NULL; - } - - os->session = ps; - os->type = device->type; - - ps->output_session = os; ps->state = PA_STREAM_UNCONNECTED; ps->device = device; ps->status_cb = cb; @@ -178,6 +160,8 @@ pulse_session_make(struct output_device *device, output_status_cb cb) ps->next = sessions; sessions = ps; + device->session = ps; + return ps; } diff --git a/src/outputs/raop.c b/src/outputs/raop.c index 4f228980..f8fb64e8 100644 --- a/src/outputs/raop.c +++ b/src/outputs/raop.c @@ -80,14 +80,6 @@ #include "raop_verification.h" #endif -// AirTunes v2 packet interval in ns */ -// (352 samples/packet * 1e9 ns/s) / 44100 samples/s = 7981859 ns/packet -// #define AIRTUNES_V2_STREAM_PERIOD 7981859 - -#ifndef MIN -# define MIN(a, b) ((a < b) ? a : b) -#endif - #define ALAC_HEADER_LEN 3 #define RAOP_QUALITY_SAMPLE_RATE_DEFAULT 44100 @@ -220,9 +212,7 @@ struct raop_session 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; /* AirTunes v2 */ @@ -320,6 +310,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; @@ -355,16 +353,10 @@ static struct timeval keep_alive_tv = { 30, 0 }; static struct raop_master_session *raop_master_sessions; static struct raop_session *raop_sessions; -/* 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 }; - // Forwards static int raop_device_start(struct output_device *rd, output_status_cb cb); -static void -raop_device_stop(struct output_session *session); - /* ------------------------------- MISC HELPERS ----------------------------- */ @@ -1763,7 +1755,7 @@ raop_status(struct raop_session *rs) rs->status_cb = NULL; if (status_cb) - status_cb(rs->device, rs->output_session, state); + status_cb(rs->device, state); if (rs->state == RAOP_STATE_UNVERIFIED) player_speaker_status_trigger(); @@ -1882,8 +1874,6 @@ session_free(struct raop_session *rs) if (rs->devname) free(rs->devname); - free(rs->output_session); - free(rs); } @@ -1905,6 +1895,8 @@ session_cleanup(struct raop_session *rs) s->next = rs->next; } + rs->device->session = NULL; + session_free(rs); } @@ -1920,14 +1912,6 @@ session_failure(struct raop_session *rs) session_cleanup(rs); } -static void -session_failure_cb(struct evrtsp_request *req, void *arg) -{ - struct raop_session *rs = arg; - - session_failure(rs); -} - static void deferredev_cb(int fd, short what, void *arg) { @@ -1939,23 +1923,67 @@ deferredev_cb(int fd, short what, void *arg) } static void -raop_rtsp_close_cb(struct evrtsp_connection *evcon, void *arg) +deferred_session_failure(struct raop_session *rs) { - 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 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; + struct output_device *rd = rs->device; + output_status_cb status_cb = rs->status_cb; + + rs->reqs_in_flight--; + + if (!req || 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); + + // Clean up session before giving status to the user (good to get rid of it if he + // calls back into us - e.g. tries to make a new session in the callback) + session_cleanup(rs); + + // Can't use raop_status() here, sadly + if (status_cb) + status_cb(rd, OUTPUT_STATE_STOPPED); + + return; +} + +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 struct raop_session * session_make(struct output_device *rd, int family, output_status_cb cb, bool only_probe) { - struct output_session *os; struct raop_session *rs; struct raop_extra *re; char *address; @@ -1988,25 +2016,8 @@ session_make(struct output_device *rd, int family, output_status_cb cb, bool onl 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))); - 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; @@ -2135,9 +2146,13 @@ session_make(struct output_device *rd, int family, output_status_cb cb, bool onl goto out_free_evcon; } + // Attach to list of sessions rs->next = raop_sessions; raop_sessions = rs; + // rs is now the official device session + rd->session = rs; + return rs; out_free_evcon: @@ -2717,15 +2732,10 @@ raop_cb_set_volume(struct evrtsp_request *req, void *arg) static int raop_set_volume_one(struct output_device *rd, output_status_cb cb) { - struct raop_session *rs; + struct raop_session *rs = rd->session; int ret; - if (!rd->session || !rd->session->session) - return 0; - - rs = rd->session->session; - - if (!(rs->state & RAOP_STATE_F_CONNECTED)) + if (!rs || !(rs->state & RAOP_STATE_F_CONNECTED)) return 0; ret = raop_set_volume_internal(rs, rd->volume, raop_cb_set_volume); @@ -2813,12 +2823,7 @@ raop_flush_timer_cb(int fd, short what, void *arg) DPRINTF(E_DBG, L_RAOP, "Flush timer expired; tearing down RAOP sessions\n"); for (rs = raop_sessions; rs; rs = rs->next) - { - if (!(rs->state & RAOP_STATE_F_CONNECTED)) - continue; - - raop_device_stop(rs->output_session); - } + session_teardown(rs, "raop_flush_timer_cb"); } static void @@ -2882,7 +2887,6 @@ packet_prepare(struct rtp_packet *pkt, uint8_t *rawbuf, size_t rawbuf_size, bool static int packet_send(struct raop_session *rs, struct rtp_packet *pkt) { - struct timeval tv; int ret; if (!rs) @@ -2893,12 +2897,9 @@ packet_send(struct raop_session *rs, struct rtp_packet *pkt) { DPRINTF(E_LOG, L_RAOP, "Send error for '%s': %s\n", rs->devname, strerror(errno)); - rs->state = RAOP_STATE_FAILED; - // Can't free it right away, it would make the ->next in the calling // master_session and session loops invalid - evutil_timerclear(&tv); - evtimer_add(rs->deferredev, &tv); + deferred_session_failure(rs); return -1; } else if (ret != pkt->data_len) @@ -2943,8 +2944,6 @@ control_packet_send(struct raop_session *rs, struct rtp_packet *pkt) 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)); - - DPRINTF(E_DBG, L_PLAYER, "SYNC PACKET SENT\n"); } static void @@ -2990,7 +2989,6 @@ packets_send(struct raop_master_session *rms) { pkt->header[1] = 0xe0; packet_send(rs, pkt); - rs->state = RAOP_STATE_STREAMING; } else if (rs->state == RAOP_STATE_STREAMING) { @@ -3006,7 +3004,7 @@ packets_send(struct raop_master_session *rms) } static void -packets_sync_send(struct raop_master_session *rms, struct timespec *pts) +packets_sync_send(struct raop_master_session *rms, struct timespec pts) { struct rtp_packet *sync_pkt; struct raop_session *rs; @@ -3022,9 +3020,9 @@ packets_sync_send(struct raop_master_session *rms, struct timespec *pts) // 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 do we need this? could we just send the future timestamp? - cur_stamp.ts.tv_sec = pts->tv_sec - OUTPUTS_BUFFER_DURATION; - cur_stamp.ts.tv_nsec = pts->tv_nsec; + cur_stamp.ts.tv_sec = pts.tv_sec - OUTPUTS_BUFFER_DURATION; + cur_stamp.ts.tv_nsec = pts.tv_nsec; + // 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 @@ -3041,6 +3039,8 @@ packets_sync_send(struct raop_master_session *rms, struct timespec *pts) { 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 ", rtptime=%" PRIu32 "\n", rs->devname, cur_stamp.pos, rms->rtp_session->pos); } else if (is_sync_time && rs->state == RAOP_STATE_STREAMING) { @@ -3553,9 +3553,32 @@ raop_v2_control_start(int v6enabled) /* ------------------------------ Session startup --------------------------- */ +static void +raop_cb_startup_retry(struct evrtsp_request *req, void *arg) +{ + struct raop_session *rs = arg; + struct output_device *rd = rs->device; + output_status_cb cb = rs->status_cb; + + session_cleanup(rs); + raop_device_start(rd, cb); +} + +static void +raop_cb_startup_cancel(struct evrtsp_request *req, void *arg) +{ + struct raop_session *rs = arg; + + session_failure(rs); +} + static void raop_startup_cancel(struct raop_session *rs) { + struct output_device *rd = rs->device; + output_status_cb cb; + int ret; + if (!rs->session) { session_failure(rs); @@ -3567,20 +3590,24 @@ raop_startup_cancel(struct raop_session *rs) 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; + rd->v6_disabled = 1; - // Be nice to our peer + session_failure_cb() cleans up old session - raop_send_req_teardown(rs, session_failure_cb, "startup_cancel"); + // Stop current session and wait for call back + ret = raop_send_req_teardown(rs, raop_cb_startup_retry, "startup_cancel"); + if (ret < 0) + { + // No connection at all, lets clean up and try again + cb = rs->status_cb; + session_cleanup(rs); + raop_device_start(rd, cb); + } - // Try to start a new session - raop_device_start(rs->device, rs->status_cb); - - // Don't let the failed session make a negative status callback - rs->status_cb = NULL; return; } - raop_send_req_teardown(rs, session_failure_cb, "startup_cancel"); + ret = raop_send_req_teardown(rs, raop_cb_startup_cancel, "startup_cancel"); + if (ret < 0) + session_failure(rs); } static void @@ -4069,41 +4096,6 @@ raop_cb_startup_options(struct evrtsp_request *req, void *arg) 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); - - session_cleanup(rs); - - return; - - error: - session_failure(rs); -} - /* ------------------------- tvOS device verification ----------------------- */ /* e.g. for the ATV4 (read it from the bottom and up) */ @@ -4801,51 +4793,51 @@ raop_device_probe(struct output_device *rd, output_status_cb cb) static int raop_device_start(struct output_device *rd, output_status_cb cb) { - event_del(flush_timer); - evtimer_add(keep_alive_timer, &keep_alive_tv); - return raop_device_start_generic(rd, cb, 0); } -static void -raop_device_stop(struct output_session *session) +static int +raop_device_stop(struct output_device *rd, output_status_cb cb) { - struct raop_session *rs = session->session; + struct raop_session *rs = rd->session; - if (rs->state & RAOP_STATE_F_CONNECTED) - raop_send_req_teardown(rs, raop_cb_shutdown_teardown, "device_stop"); - else - session_cleanup(rs); + rs->status_cb = cb; + + return session_teardown(rs, "device_stop"); } static void -raop_device_free_extra(struct output_device *device) +raop_device_free_extra(struct output_device *rd) { - struct raop_extra *re = device->extra_device_info; + struct raop_extra *re = rd->extra_device_info; free(re); } +static void +raop_device_set_cb(struct output_device *rd, output_status_cb cb) +{ + struct raop_session *rs = rd->session; + + rs->status_cb = cb; +} + static void raop_playback_stop(void) { struct raop_session *rs; - int ret; evtimer_del(keep_alive_timer); 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"); - } + session_teardown(rs, "playback_stop"); } static void raop_write(struct output_buffer *obuf) { struct raop_master_session *rms; + struct raop_session *rs; int i; for (rms = raop_master_sessions; rms; rms = rms->next) @@ -4871,6 +4863,19 @@ raop_write(struct output_buffer *obuf) } } } + + // 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) + { + if (rs->state != RAOP_STATE_CONNECTED) + continue; + + event_del(flush_timer); // In case playback was stopped but then restarted again + + rs->state = RAOP_STATE_STREAMING; + // Make a cb? + } } static int @@ -4894,7 +4899,6 @@ raop_flush(output_status_cb cb) if (ret < 0) { session_failure(rs); - continue; } @@ -4912,14 +4916,6 @@ raop_flush(output_status_cb cb) return pending; } -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) { @@ -5035,7 +5031,6 @@ raop_init(void) goto out_stop_control; } - return 0; out_stop_control: @@ -5087,16 +5082,16 @@ struct output_definition output_raop = .disabled = 0, .init = raop_init, .deinit = raop_deinit, - .device_start2 = raop_device_start, + .device_start = raop_device_start, .device_stop = raop_device_stop, .device_probe = raop_device_probe, .device_free_extra = raop_device_free_extra, .device_volume_set = raop_set_volume_one, .device_volume_to_pct = raop_volume_to_pct, + .device_set_cb = raop_device_set_cb, .playback_stop = raop_playback_stop, - .write2 = raop_write, - .flush2 = raop_flush, - .status_cb = raop_set_status_cb, + .write = raop_write, + .flush = raop_flush, .metadata_prepare = raop_metadata_prepare, .metadata_send = raop_metadata_send, .metadata_purge = raop_metadata_purge, diff --git a/src/outputs/rtp_common.c b/src/outputs/rtp_common.c index ecfa3146..20074608 100644 --- a/src/outputs/rtp_common.c +++ b/src/outputs/rtp_common.c @@ -252,14 +252,14 @@ rtp_sync_packet_next(struct rtp_session *session, struct rtcp_timestamp *cur_sta 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", +/* 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/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 295514d3..2f66411f 100644 --- a/src/player.c +++ b/src/player.c @@ -287,6 +287,8 @@ playback_abort(void); static void playback_suspend(void); +static int +player_get_current_pos(uint64_t *pos, struct timespec *ts, int commit); /* ----------------------------- Volume helpers ----------------------------- */ @@ -1009,7 +1011,7 @@ source_switch(int nbytes) } static void -session_init(struct player_session *session, struct media_quality *quality) +session_reset(struct player_session *session, struct media_quality *quality) { session->samples_written = 0; session->quality = *quality; @@ -1033,7 +1035,7 @@ session_init(struct player_session *session, struct media_quality *quality) } static void -session_deinit(struct player_session *session) +session_clear(struct player_session *session) { free(session->buffer); memset(session, 0, sizeof(struct player_session)); @@ -1083,7 +1085,9 @@ source_read(uint8_t *buf, int len) else if (flags & INPUT_FLAG_QUALITY) { input_quality_get(&quality); - session_init(&pb_session, &quality); + + if (!quality_is_equal(&quality, &pb_session.quality)) + session_reset(&pb_session, &quality); } // We pad the output buffer with silence if we don't have enough data for a @@ -1171,7 +1175,7 @@ playback_cb(int fd, short what, void *arg) } nsamples = BTOS(got, pb_session.quality.bits_per_sample, pb_session.quality.channels); - outputs_write2(pb_session.buffer, pb_session.bufsize, &pb_session.quality, nsamples, &pb_session.pts); + outputs_write(pb_session.buffer, pb_session.bufsize, &pb_session.quality, nsamples, &pb_session.pts); pb_session.samples_written += nsamples; if (got < pb_session.bufsize) @@ -1468,11 +1472,11 @@ device_metadata_send(void *arg, int *retval) /* -------- 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)); + DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_streaming_cb (status %d)\n", outputs_name(device->type), status); ret = device_check(device); if (ret < 0) @@ -1492,8 +1496,6 @@ 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); @@ -1506,24 +1508,22 @@ device_streaming_cb(struct output_device *device, struct output_session *session output_sessions--; - device->session = NULL; - if (!device->advertised) device_remove(device); } else - outputs_status_cb(session, device_streaming_cb); + outputs_device_set_cb(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)); + DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_command_cb (status %d)\n", outputs_name(device->type), status); - outputs_status_cb(session, device_streaming_cb); + outputs_device_set_cb(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 if (player_flush_pending > 0) @@ -1537,12 +1537,12 @@ device_command_cb(struct output_device *device, struct output_session *session, } 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)); + DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_shutdown_cb (status %d)\n", outputs_name(device->type), status); if (output_sessions) output_sessions--; @@ -1558,8 +1558,6 @@ device_shutdown_cb(struct output_device *device, struct output_session *session, goto out; } - device->session = NULL; - if (!device->advertised) device_remove(device); @@ -1572,9 +1570,9 @@ 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_lost_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)); + DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_lost_cb (status %d)\n", outputs_name(device->type), status); // We lost that device during startup for some reason, not much we can do here if (status == OUTPUT_STATE_FAILED) @@ -1584,12 +1582,12 @@ device_lost_cb(struct output_device *device, struct output_session *session, enu } static void -device_activate_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) { int retval; int ret; - DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_activate_cb\n", outputs_name(device->type)); + DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_activate_cb (status %d)\n", outputs_name(device->type), status); retval = commands_exec_returnvalue(cmdbase); ret = device_check(device); @@ -1597,8 +1595,7 @@ device_activate_cb(struct output_device *device, struct output_session *session, { DPRINTF(E_WARN, L_PLAYER, "Output device disappeared during startup!\n"); - outputs_status_cb(session, device_lost_cb); - outputs_device_stop(session); + outputs_device_stop(device, device_lost_cb); if (retval != -2) retval = -1; @@ -1623,11 +1620,9 @@ device_activate_cb(struct output_device *device, struct output_session *session, goto out; } - device->session = session; - output_sessions++; - outputs_status_cb(session, device_streaming_cb); + outputs_device_set_cb(device, device_streaming_cb); out: /* cur_cmd->ret already set @@ -1639,12 +1634,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)); + DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_probe_cb (status %d)\n", outputs_name(device->type), status); retval = commands_exec_returnvalue(cmdbase); ret = device_check(device); @@ -1685,12 +1680,12 @@ 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)); + DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_restart_cb (status %d)\n", outputs_name(device->type), status); retval = commands_exec_returnvalue(cmdbase); ret = device_check(device); @@ -1698,8 +1693,7 @@ device_restart_cb(struct output_device *device, struct output_session *session, { DPRINTF(E_WARN, L_PLAYER, "Output device disappeared during restart!\n"); - outputs_status_cb(session, device_lost_cb); - outputs_device_stop(session); + outputs_device_stop(device, device_lost_cb); if (retval != -2) retval = -1; @@ -1724,10 +1718,8 @@ device_restart_cb(struct output_device *device, struct output_session *session, goto out; } - device->session = session; - output_sessions++; - outputs_status_cb(session, device_streaming_cb); + outputs_device_set_cb(device, device_streaming_cb); out: commands_exec_end(cmdbase, retval); @@ -1786,19 +1778,30 @@ 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 int +pb_session_stop(void) +{ + int ret; + + ret = playback_timer_stop(); + + session_clear(&pb_session); + + return ret; +} + static void playback_abort(void) { outputs_playback_stop(); - playback_timer_stop(); + pb_session_stop(); source_stop(); @@ -1815,9 +1818,9 @@ playback_abort(void) static void playback_suspend(void) { - player_flush_pending = outputs_flush2(device_command_cb); + player_flush_pending = outputs_flush(device_command_cb); - playback_timer_stop(); + pb_session_stop(); status_update(PLAY_PAUSED); @@ -1949,9 +1952,9 @@ playback_stop(void *arg, int *retval) // 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_flush2(device_command_cb); + *retval = outputs_flush(device_command_cb); - playback_timer_stop(); + pb_session_stop(); ps_playing = source_now_playing(); if (ps_playing) @@ -1975,7 +1978,6 @@ playback_stop(void *arg, int *retval) static enum command_state playback_start_bh(void *arg, int *retval) { - struct timespec ts; int ret; // initialize the packet timer to the same relative time that we have @@ -1989,10 +1991,6 @@ playback_start_bh(void *arg, int *retval) // pb_buffer_offset = 0; pb_read_deficit = 0; - ret = clock_gettime_with_res(CLOCK_MONOTONIC, &ts, &player_timer_res); - if (ret < 0) - goto out_fail; - ret = playback_timer_start(); if (ret < 0) goto out_fail; @@ -2098,7 +2096,7 @@ playback_start_item(void *arg, int *retval) { if (device->selected && !device->session) { - ret = outputs_device_start2(device, device_restart_cb); + 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); @@ -2118,7 +2116,7 @@ playback_start_item(void *arg, int *retval) continue; speaker_select_output(device); - ret = outputs_device_start(device, device_restart_cb, TEMP_NEXT_RTPTIME); + 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); @@ -2420,9 +2418,9 @@ playback_pause(void *arg, int *retval) return COMMAND_END; } - *retval = outputs_flush2(device_command_cb); + *retval = outputs_flush(device_command_cb); - playback_timer_stop(); + pb_session_stop(); source_pause(pos); @@ -2530,7 +2528,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_start2(device, device_activate_cb); + 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); @@ -2568,8 +2566,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; } @@ -2996,7 +2993,8 @@ playerqueue_plid(void *arg, int *retval) /* ------------------------------- Player API ------------------------------- */ -int +// TODO no longer part of API +static int player_get_current_pos(uint64_t *pos, struct timespec *ts, int commit) { uint64_t delta; @@ -3039,21 +3037,6 @@ player_get_current_pos(uint64_t *pos, struct timespec *ts, int commit) return 0; } -int -player_get_time(struct timespec *ts) -{ - int ret; - - ret = clock_gettime_with_res(CLOCK_MONOTONIC, ts, &player_timer_res); - if (ret < 0) - { - DPRINTF(E_LOG, L_PLAYER, "Couldn't get clock: %s\n", strerror(errno)); - return -1; - } - - return 0; -} - int player_get_status(struct player_status *status) { @@ -3491,7 +3474,6 @@ player(void *arg) int player_init(void) { - struct media_quality default_quality = { 44100, 16, 2 }; uint64_t interval; uint32_t rnd; int ret; @@ -3567,8 +3549,6 @@ player_init(void) goto evnew_fail; } - session_init(&pb_session, &default_quality); - cmdbase = commands_base_new(evbase_player, NULL); ret = outputs_init(); @@ -3605,7 +3585,6 @@ player_init(void) outputs_deinit(); outputs_fail: commands_base_free(cmdbase); - session_deinit(&pb_session); evnew_fail: event_base_free(evbase_player); evbase_fail: @@ -3638,7 +3617,7 @@ player_deinit(void) player_exit = 1; commands_base_destroy(cmdbase); - session_deinit(&pb_session); + session_clear(&pb_session); ret = pthread_join(tid_player, NULL); if (ret != 0) diff --git a/src/player.h b/src/player.h index e8347e4d..3dc6eee4 100644 --- a/src/player.h +++ b/src/player.h @@ -72,13 +72,6 @@ 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_time(struct timespec *ts); - int player_get_status(struct player_status *status); From a7e8476996b7fb41ee8c57d58cb204d2515958c8 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 12 Feb 2019 21:25:27 +0100 Subject: [PATCH 20/86] [outputs] Refactor outputs some more, eg change callback system --- src/input.c | 3 - src/outputs.c | 457 ++++++++++++++++++++++++++++++++------------ src/outputs.h | 71 ++++--- src/outputs/alsa.c | 2 +- src/outputs/cast.c | 3 +- src/outputs/dummy.c | 96 +++------- src/outputs/fifo.c | 95 +++------ src/outputs/pulse.c | 2 +- src/outputs/raop.c | 184 ++++++++++-------- src/player.c | 88 +++++---- src/player.h | 6 +- 11 files changed, 589 insertions(+), 418 deletions(-) diff --git a/src/input.c b/src/input.c index cda5f13b..42173fba 100644 --- a/src/input.c +++ b/src/input.c @@ -267,9 +267,6 @@ input_write(struct evbuffer *evbuf, struct media_quality *quality, short flags) return 0; } - // Change of quality. Note, the marker is placed at the last position of the - // last byte we wrote, even though that of course doesn't have the new quality - // yet. Not intuitive, but input_read() will understand. if (quality && !quality_is_equal(quality, &input_buffer.cur_write_quality)) { input_buffer.cur_write_quality = *quality; diff --git a/src/outputs.c b/src/outputs.c index c0eb861f..342bfa05 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -29,9 +29,13 @@ #include #include +#include + #include "logger.h" #include "misc.h" #include "transcode.h" +#include "listener.h" +#include "player.h" //TODO remove me when player_pmap is removed again #include "outputs.h" extern struct output_definition output_raop; @@ -48,6 +52,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, @@ -66,6 +73,24 @@ static struct output_definition *outputs[] = { NULL }; +#define OUTPUTS_MAX_CALLBACKS 64 + +struct outputs_callback_queue +{ + 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 outputs_callback_queue outputs_cb_queue[OUTPUTS_MAX_CALLBACKS]; +struct event *outputs_deferredev; + struct output_quality_subscription { int count; @@ -79,6 +104,115 @@ static bool output_got_new_subscription; /* ------------------------------- MISC HELPERS ----------------------------- */ +static void +callback_remove(struct output_device *device) +{ + int callback_id; + + if (!device) + return; + + for (callback_id = 0; callback_id < ARRAY_SIZE(outputs_cb_queue); callback_id++) + { + if (outputs_cb_queue[callback_id].device == device) + { + DPRINTF(E_DBG, L_PLAYER, "Removing callback to %s, id %d\n", player_pmap(outputs_cb_queue[callback_id].cb), callback_id); + memset(&outputs_cb_queue[callback_id], 0, sizeof(struct outputs_callback_queue)); + } + } +} + +static int +callback_add(struct output_device *device, output_status_cb cb) +{ + int callback_id; + + if (!cb) + return -1; + + // 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_queue); callback_id++) + { + if (outputs_cb_queue[callback_id].cb == NULL) + break; + } + + if (callback_id == ARRAY_SIZE(outputs_cb_queue)) + { + DPRINTF(E_LOG, L_PLAYER, "Output callback queue is full! (size is %d)\n", OUTPUTS_MAX_CALLBACKS); + return -1; + } + + outputs_cb_queue[callback_id].cb = cb; + outputs_cb_queue[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\n", player_pmap(cb), callback_id); + + int active = 0; + for (int i = 0; i < ARRAY_SIZE(outputs_cb_queue); i++) + if (outputs_cb_queue[i].cb) + active++; + + DPRINTF(E_DBG, L_PLAYER, "Number of active callbacks: %d\n", active); + + return callback_id; +}; + +static void +callback_remove_all(enum output_types type) +{ + struct output_device *device; + + for (device = output_device_list; device; device = device->next) + { + if (type != device->type) + continue; + + outputs_device_cb_set(device, NULL); + + callback_remove(device); + } +} + +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_queue); callback_id++) + { + if (outputs_cb_queue[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_queue[callback_id].cb; + state = outputs_cb_queue[callback_id].state; + + // Will be NULL if the device has disappeared + device = outputs_device_get(outputs_cb_queue[callback_id].device_id); + + memset(&outputs_cb_queue[callback_id], 0, sizeof(struct outputs_callback_queue)); + + 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_queue); i++) + { + if (outputs_cb_queue[i].cb) + DPRINTF(E_DBG, L_PLAYER, "%d. Active callback: %s\n", i, player_pmap(outputs_cb_queue[i].cb)); + } +} + static enum transcode_profile quality_to_xcode(struct media_quality *quality) { @@ -206,6 +340,155 @@ buffer_drain(struct output_buffer *obuf) /* ----------------------------------- 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_session_remove(uint64_t device_id) +{ + 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? + output_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; + + 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_queue)) || !outputs_cb_queue[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_queue[callback_id].ready = true; + outputs_cb_queue[callback_id].device_id = device_id; + outputs_cb_queue[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 ---------------------------- */ + int outputs_device_start(struct output_device *device, output_status_cb cb) { @@ -218,7 +501,7 @@ outputs_device_start(struct output_device *device, output_status_cb cb) return -1; } - return outputs[device->type]->device_start(device, cb); + return outputs[device->type]->device_start(device, callback_add(device, cb)); } int @@ -233,7 +516,7 @@ outputs_device_stop(struct output_device *device, output_status_cb cb) return -1; } - return outputs[device->type]->device_stop(device, cb); + return outputs[device->type]->device_stop(device, callback_add(device, cb)); } int @@ -248,7 +531,46 @@ outputs_device_probe(struct output_device *device, output_status_cb cb) return -1; } - return outputs[device->type]->device_probe(device, cb); + 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 @@ -274,52 +596,6 @@ outputs_device_free(struct output_device *device) free(device); } -int -outputs_device_volume_set(struct output_device *device, output_status_cb cb) -{ - if (outputs[device->type]->disabled) - return -1; - - if (outputs[device->type]->device_volume_set) - return outputs[device->type]->device_volume_set(device, cb); - else - return -1; -} - -int -outputs_device_volume_to_pct(struct output_device *device, const char *volume) -{ - if (outputs[device->type]->disabled) - return -1; - - if (outputs[device->type]->device_volume_to_pct) - return outputs[device->type]->device_volume_to_pct(device, volume); - else - return -1; -} - -int -outputs_device_quality_set(struct output_device *device, struct media_quality *quality) -{ - if (outputs[device->type]->disabled) - return -1; - - if (outputs[device->type]->quality_set) - return outputs[device->type]->quality_set(device, quality); - else - return -1; -} - -void -outputs_device_set_cb(struct output_device *device, output_status_cb cb) -{ - if (outputs[device->type]->disabled) - return; - - if (outputs[device->type]->device_set_cb) - outputs[device->type]->device_set_cb(device, cb); -} - void outputs_playback_stop(void) { @@ -363,11 +639,13 @@ outputs_flush(output_status_cb cb) ret = 0; for (i = 0; outputs[i]; i++) { - if (outputs[i]->disabled) + if (outputs[i]->disabled || !outputs[i]->flush) continue; - if (outputs[i]->flush) - ret += outputs[i]->flush(cb); + // Clear callback register for all devices belonging to outputs[i] + callback_remove_all(outputs[i]->type); + + ret += outputs[i]->flush(callback_add(NULL, cb)); } return ret; @@ -489,77 +767,6 @@ outputs_authorize(enum output_types type, const char *pin) outputs[type]->authorize(pin); } -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? - output_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; - - 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]; -} - int outputs_priority(struct output_device *device) { @@ -579,6 +786,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++) { @@ -615,6 +824,8 @@ outputs_deinit(void) { int i; + evtimer_del(outputs_deferredev); + for (i = 0; outputs[i]; i++) { if (outputs[i]->disabled) diff --git a/src/outputs.h b/src/outputs.h index c6763b49..9997cfea 100644 --- a/src/outputs.h +++ b/src/outputs.h @@ -21,7 +21,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 @@ -150,6 +149,7 @@ struct output_metadata { enum output_types type; void *metadata; + struct output_metadata *next; }; @@ -169,7 +169,6 @@ struct output_buffer struct output_frame frames[OUTPUTS_MAX_QUALITY_SUBSCRIPTIONS + 1]; } output_buffer; - typedef void (*output_status_cb)(struct output_device *device, enum output_device_state status); struct output_definition @@ -195,38 +194,37 @@ 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); + int (*device_start)(struct output_device *device, int callback_id); // Close a session prepared by device_start and call back - int (*device_stop)(struct output_device *device, output_status_cb cb); + int (*device_stop)(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); // Request a change of quality from the device - int (*quality_set)(struct output_device *device, struct media_quality *quality); + 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_set_cb)(struct output_device *device, output_status_cb cb); + void (*device_cb_set)(struct output_device *device, int callback_id); + + // Free the private device data + void (*device_free_extra)(struct output_device *device); // Start/stop playback on devices that were started - void (*playback_start)(uint64_t next_pkt, struct timespec *ts); void (*playback_stop)(void); // Write stream data to the output devices void (*write)(struct output_buffer *buffer); // Flush all sessions, the return must be number of sessions pending the flush - int (*flush)(output_status_cb cb); + int (*flush)(int callback_id); // Authorize an output with a pin-code (probably coming from the filescanner) void (*authorize)(const char *pin); @@ -238,6 +236,36 @@ struct output_definition 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_session_add(uint64_t device_id, void *session); + +void +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 ---------------------------- */ + int outputs_device_start(struct output_device *device, output_status_cb cb); @@ -247,21 +275,20 @@ outputs_device_stop(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); -// TODO should this function have a callback? int -outputs_device_quality_set(struct output_device *device, struct media_quality *quality); +outputs_device_quality_set(struct output_device *device, struct media_quality *quality, output_status_cb cb); void -outputs_device_set_cb(struct output_device *device, output_status_cb cb); +outputs_device_cb_set(struct output_device *device, output_status_cb cb); + +void +outputs_device_free(struct output_device *device); void outputs_playback_stop(void); @@ -290,12 +317,6 @@ outputs_metadata_free(struct output_metadata *omd); void outputs_authorize(enum output_types type, const char *pin); -int -outputs_quality_subscribe(struct media_quality *quality); - -void -outputs_quality_unsubscribe(struct media_quality *quality); - int outputs_priority(struct output_device *device); diff --git a/src/outputs/alsa.c b/src/outputs/alsa.c index 9294fcff..35a8a46c 100644 --- a/src/outputs/alsa.c +++ b/src/outputs/alsa.c @@ -196,7 +196,7 @@ alsa_session_make(struct output_device *device, output_status_cb cb) as->next = sessions; sessions = as; - device->session = as; + outputs_device_session_add(device, as); return as; diff --git a/src/outputs/cast.c b/src/outputs/cast.c index 7ddb7b65..b4940740 100644 --- a/src/outputs/cast.c +++ b/src/outputs/cast.c @@ -1420,7 +1420,8 @@ cast_session_make(struct output_device *device, int family, output_status_cb cb) cs->next = sessions; sessions = cs; - device->session = cs; + // cs is now the official session for the device + outputs_device_session_add(device, cs); proto = gnutls_protocol_get_name(gnutls_protocol_get_version(cs->tls_session)); diff --git a/src/outputs/dummy.c b/src/outputs/dummy.c index 78f24553..591eb18d 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,22 +43,12 @@ struct dummy_session { enum output_device_state state; - struct event *deferredev; - output_status_cb defer_cb; - - struct output_device *device; - 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 @@ -68,8 +57,6 @@ dummy_session_free(struct dummy_session *ds) if (!ds) return; - event_free(ds->deferredev); - free(ds); } @@ -79,38 +66,25 @@ dummy_session_cleanup(struct dummy_session *ds) // Normally some here code to remove from linked list - here we just say: sessions = NULL; - ds->device->session = 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 dummy_session *ds; - ds = calloc(1, sizeof(struct dummy_session)); - if (!ds) - { - DPRINTF(E_LOG, L_LAUDIO, "Out of memory for dummy session (as)\n"); - 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(ds); - return NULL; - } + CHECK_NULL(L_LAUDIO, ds = calloc(1, sizeof(struct dummy_session))); ds->state = OUTPUT_STATE_CONNECTED; - ds->device = device; - ds->status_cb = cb; + ds->device_id = device->id; + ds->callback_id = callback_id; sessions = ds; - device->session = ds; + outputs_device_session_add(device->id, ds); return ds; } @@ -118,37 +92,24 @@ dummy_session_make(struct output_device *device, output_status_cb cb) /* ---------------------------- 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->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) +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; @@ -158,26 +119,28 @@ dummy_device_start(struct output_device *device, output_status_cb cb) } static int -dummy_device_stop(struct output_device *device, output_status_cb cb) +dummy_device_stop(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, output_status_cb cb) +dummy_device_probe(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; - ds->status_cb = cb; + ds->callback_id = callback_id; ds->state = OUTPUT_STATE_STOPPED; dummy_status(ds); @@ -186,25 +149,25 @@ 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_volume_set(struct output_device *device, int callback_id) { struct dummy_session *ds = device->session; if (!ds) return 0; - ds->status_cb = cb; + ds->callback_id = callback_id; dummy_status(ds); return 1; } static void -dummy_device_set_cb(struct output_device *device, output_status_cb cb) +dummy_device_cb_set(struct output_device *device, int callback_id) { struct dummy_session *ds = device->session; - ds->status_cb = cb; + ds->callback_id = callback_id; } static void @@ -234,12 +197,7 @@ 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); @@ -273,6 +231,6 @@ struct output_definition output_dummy = .device_stop = dummy_device_stop, .device_probe = dummy_device_probe, .device_volume_set = dummy_device_volume_set, - .device_set_cb = dummy_device_set_cb, + .device_cb_set = dummy_device_cb_set, .playback_stop = dummy_playback_stop, }; diff --git a/src/outputs/fifo.c b/src/outputs/fifo.c index f0660772..ccb31e9b 100644 --- a/src/outputs/fifo.c +++ b/src/outputs/fifo.c @@ -31,8 +31,6 @@ #include #include -#include - #include "misc.h" #include "conffile.h" #include "logger.h" @@ -95,22 +93,12 @@ struct fifo_session int created; - struct event *deferredev; - output_status_cb defer_cb; - - struct output_device *device; - 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 ---------------------------- */ @@ -242,8 +230,6 @@ fifo_session_free(struct fifo_session *fifo_session) if (!fifo_session) return; - event_free(fifo_session->deferredev); - free(fifo_session); free_buffer(); } @@ -254,29 +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; - fifo_session->device->session = 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 fifo_session *fifo_session; CHECK_NULL(L_FIFO, fifo_session = calloc(1, sizeof(struct fifo_session))); - 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(fifo_session); - return NULL; - } - 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; @@ -285,7 +263,7 @@ fifo_session_make(struct output_device *device, output_status_cb cb) sessions = fifo_session; - device->session = fifo_session; + outputs_device_session_add(device->id, fifo_session); return fifo_session; } @@ -293,33 +271,19 @@ fifo_session_make(struct output_device *device, output_status_cb cb) /* ---------------------------- 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->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) +fifo_device_start(struct output_device *device, int callback_id) { struct fifo_session *fifo_session; int ret; @@ -328,7 +292,7 @@ fifo_device_start(struct output_device *device, output_status_cb cb) if (ret < 0) return -1; - fifo_session = fifo_session_make(device, cb); + fifo_session = fifo_session_make(device, callback_id); if (!fifo_session) return -1; @@ -342,13 +306,13 @@ fifo_device_start(struct output_device *device, output_status_cb cb) } static int -fifo_device_stop(struct output_device *device, output_status_cb cb) +fifo_device_stop(struct output_device *device, int callback_id) { struct fifo_session *fifo_session = device->session; outputs_quality_unsubscribe(&fifo_quality); - fifo_session->status_cb = cb; + fifo_session->callback_id = callback_id; fifo_close(fifo_session); free_buffer(); @@ -360,12 +324,12 @@ fifo_device_stop(struct output_device *device, output_status_cb cb) } static int -fifo_device_probe(struct output_device *device, output_status_cb cb) +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; @@ -378,7 +342,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); @@ -387,25 +351,25 @@ 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 = device->session; if (!fifo_session) return 0; - fifo_session->status_cb = cb; + fifo_session->callback_id = callback_id; fifo_status(fifo_session); return 1; } static void -fifo_device_set_cb(struct output_device *device, output_status_cb cb) +fifo_device_cb_set(struct output_device *device, int callback_id) { struct fifo_session *fifo_session = device->session; - fifo_session->status_cb = cb; + fifo_session->callback_id = callback_id; } static void @@ -423,7 +387,7 @@ fifo_playback_stop(void) } static int -fifo_flush(output_status_cb cb) +fifo_flush(int callback_id) { struct fifo_session *fifo_session = sessions; @@ -433,7 +397,7 @@ fifo_flush(output_status_cb cb) fifo_empty(fifo_session); free_buffer(); - fifo_session->status_cb = cb; + fifo_session->callback_id = callback_id; fifo_session->state = OUTPUT_STATE_CONNECTED; fifo_status(fifo_session); return 1; @@ -448,7 +412,7 @@ fifo_write(struct output_buffer *obuf) ssize_t bytes; int i; - if (!fifo_session || !fifo_session->device->selected) + if (!fifo_session) return; for (i = 0; obuf->frames[i].buffer; i++) @@ -535,12 +499,7 @@ 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); @@ -574,7 +533,7 @@ struct output_definition output_fifo = .device_stop = fifo_device_stop, .device_probe = fifo_device_probe, .device_volume_set = fifo_device_volume_set, - .device_set_cb = fifo_device_set_cb, + .device_cb_set = fifo_device_cb_set, .playback_stop = fifo_playback_stop, .write = fifo_write, .flush = fifo_flush, diff --git a/src/outputs/pulse.c b/src/outputs/pulse.c index 0a962105..2bd455d7 100644 --- a/src/outputs/pulse.c +++ b/src/outputs/pulse.c @@ -160,7 +160,7 @@ pulse_session_make(struct output_device *device, output_status_cb cb) ps->next = sessions; sessions = ps; - device->session = ps; + outputs_device_session_add(device, ps); return ps; } diff --git a/src/outputs/raop.c b/src/outputs/raop.c index f8fb64e8..512b6578 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 @@ -180,6 +180,9 @@ struct raop_master_session struct raop_session { + uint64_t device_id; + int callback_id; + struct raop_master_session *master_session; struct evrtsp_connection *ctrl; @@ -212,9 +215,6 @@ struct raop_session int volume; uint64_t start_rtptime; - struct output_device *device; - output_status_cb status_cb; - /* AirTunes v2 */ unsigned short server_port; unsigned short control_port; @@ -355,7 +355,7 @@ static struct raop_session *raop_sessions; // Forwards static int -raop_device_start(struct output_device *rd, output_status_cb cb); +raop_device_start(struct output_device *rd, int callback_id); /* ------------------------------- MISC HELPERS ----------------------------- */ @@ -1089,7 +1089,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) @@ -1725,7 +1725,6 @@ raop_send_req_pin_start(struct raop_session *rs, evrtsp_req_cb cb, const char *l static void raop_status(struct raop_session *rs) { - output_status_cb status_cb = rs->status_cb; enum output_device_state state; switch (rs->state) @@ -1749,16 +1748,16 @@ 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, 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 * @@ -1895,7 +1894,7 @@ session_cleanup(struct raop_session *rs) s->next = rs->next; } - rs->device->session = NULL; + outputs_device_session_remove(rs->device_id); session_free(rs); } @@ -1947,23 +1946,17 @@ static void session_teardown_cb(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; - struct output_device *rd = rs->device; - output_status_cb status_cb = rs->status_cb; rs->reqs_in_flight--; if (!req || 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); - // Clean up session before giving status to the user (good to get rid of it if he - // calls back into us - e.g. tries to make a new session in the callback) + rs->state = OUTPUT_STATE_STOPPED; + + raop_status(rs); + session_cleanup(rs); - - // Can't use raop_status() here, sadly - if (status_cb) - status_cb(rd, OUTPUT_STATE_STOPPED); - - return; } static int @@ -1982,7 +1975,7 @@ session_teardown(struct raop_session *rs, const char *log_caller) } static struct raop_session * -session_make(struct output_device *rd, int family, output_status_cb cb, bool only_probe) +session_make(struct output_device *rd, int family, int callback_id, bool only_probe) { struct raop_session *rs; struct raop_extra *re; @@ -2023,8 +2016,9 @@ session_make(struct output_device *rd, int family, output_status_cb cb, bool onl 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; @@ -2151,7 +2145,7 @@ session_make(struct output_device *rd, int family, output_status_cb cb, bool onl raop_sessions = rs; // rs is now the official device session - rd->session = rs; + outputs_device_session_add(rd->id, rs); return rs; @@ -2323,7 +2317,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. */ @@ -2730,15 +2724,15 @@ raop_cb_set_volume(struct evrtsp_request *req, void *arg) /* 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 = rd->session; + struct raop_session *rs = device->session; int ret; if (!rs || !(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) { session_failure(rs); @@ -2746,7 +2740,7 @@ raop_set_volume_one(struct output_device *rd, output_status_cb cb) return 0; } - rs->status_cb = cb; + rs->callback_id = callback_id; return 1; } @@ -3557,11 +3551,18 @@ static void raop_cb_startup_retry(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; - struct output_device *rd = rs->device; - output_status_cb cb = rs->status_cb; + struct output_device *device; + int callback_id = rs->callback_id; + + device = outputs_device_get(rs->device_id); + if (!device) + { + session_failure(rs); + return; + } session_cleanup(rs); - raop_device_start(rd, cb); + raop_device_start(device, callback_id); } static void @@ -3575,11 +3576,11 @@ raop_cb_startup_cancel(struct evrtsp_request *req, void *arg) static void raop_startup_cancel(struct raop_session *rs) { - struct output_device *rd = rs->device; - output_status_cb cb; + struct output_device *device; int ret; - if (!rs->session) + device = outputs_device_get(rs->device_id); + if (!device || !rs->session) { session_failure(rs); return; @@ -3590,17 +3591,12 @@ raop_startup_cancel(struct raop_session *rs) if (rs->family == AF_INET6 && !(rs->state & RAOP_STATE_F_FAILED)) { // This flag is permanent and will not be overwritten by mdns advertisements - rd->v6_disabled = 1; + 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) - { - // No connection at all, lets clean up and try again - cb = rs->status_cb; - session_cleanup(rs); - raop_device_start(rd, cb); - } + raop_cb_startup_retry(NULL, rs); // No connection at all, call retry directly return; } @@ -3995,6 +3991,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; @@ -4044,7 +4041,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) @@ -4251,6 +4252,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); @@ -4258,9 +4260,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; } @@ -4270,7 +4276,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; @@ -4283,14 +4289,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; } @@ -4311,14 +4322,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) @@ -4340,6 +4351,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; @@ -4356,11 +4368,15 @@ 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 session_cleanup(rs); @@ -4734,7 +4750,7 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha /* Thread: player */ static int -raop_device_start_generic(struct output_device *rd, output_status_cb cb, bool only_probe) +raop_device_start_generic(struct output_device *device, int callback_id, bool only_probe) { struct raop_session *rs; int ret; @@ -4744,12 +4760,12 @@ raop_device_start_generic(struct output_device *rd, output_status_cb cb, bool on * address and build our session URL for all subsequent requests. */ - rs = session_make(rd, AF_INET6, cb, only_probe); + rs = session_make(device, AF_INET6, callback_id, only_probe); if (rs) { - 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"); @@ -4763,13 +4779,13 @@ raop_device_start_generic(struct output_device *rd, output_status_cb cb, bool on } } - rs = session_make(rd, AF_INET, cb, only_probe); + rs = session_make(device, AF_INET, callback_id, only_probe); if (!rs) return -1; - 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"); @@ -4785,41 +4801,41 @@ raop_device_start_generic(struct output_device *rd, output_status_cb cb, bool on } 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, 1); + return raop_device_start_generic(device, callback_id, 1); } static int -raop_device_start(struct output_device *rd, output_status_cb cb) +raop_device_start(struct output_device *device, int callback_id) { - return raop_device_start_generic(rd, cb, 0); + return raop_device_start_generic(device, callback_id, 0); } static int -raop_device_stop(struct output_device *rd, output_status_cb cb) +raop_device_stop(struct output_device *device, int callback_id) { - struct raop_session *rs = rd->session; + struct raop_session *rs = device->session; - rs->status_cb = cb; + rs->callback_id = callback_id; return session_teardown(rs, "device_stop"); } static void -raop_device_free_extra(struct output_device *rd) +raop_device_cb_set(struct output_device *device, int callback_id) { - struct raop_extra *re = rd->extra_device_info; + struct raop_session *rs = device->session; - free(re); + rs->callback_id = callback_id; } static void -raop_device_set_cb(struct output_device *rd, output_status_cb cb) +raop_device_free_extra(struct output_device *device) { - struct raop_session *rs = rd->session; + struct raop_extra *re = device->extra_device_info; - rs->status_cb = cb; + free(re); } static void @@ -4879,7 +4895,7 @@ raop_write(struct output_buffer *obuf) } static int -raop_flush(output_status_cb cb) +raop_flush(int callback_id) { struct timeval tv; struct raop_session *rs; @@ -4902,7 +4918,7 @@ raop_flush(output_status_cb cb) continue; } - rs->status_cb = cb; + rs->callback_id = callback_id; pending++; } @@ -5085,10 +5101,10 @@ struct output_definition output_raop = .device_start = raop_device_start, .device_stop = raop_device_stop, .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, - .device_set_cb = raop_device_set_cb, .playback_stop = raop_playback_stop, .write = raop_write, .flush = raop_flush, diff --git a/src/player.c b/src/player.c index 2f66411f..6e474fd6 100644 --- a/src/player.c +++ b/src/player.c @@ -256,9 +256,6 @@ 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; @@ -326,7 +323,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); @@ -341,7 +338,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; @@ -1219,14 +1216,14 @@ device_list_sort(void) { swaps = 0; prev = NULL; - for (device = dev_list; device && device->next; device = device->next) + 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 == dev_list) - dev_list = next; + if (device == output_device_list) + output_device_list = next; if (prev) prev->next = next; @@ -1248,7 +1245,7 @@ device_remove(struct output_device *remove) int ret; prev = NULL; - for (device = dev_list; device; device = device->next) + for (device = output_device_list; device; device = device->next) { if (device == remove) break; @@ -1271,7 +1268,7 @@ device_remove(struct output_device *remove) speaker_deselect_output(remove); if (!prev) - dev_list = remove->next; + output_device_list = remove->next; else prev->next = remove->next; @@ -1283,7 +1280,7 @@ device_check(struct output_device *check) { struct output_device *device; - for (device = dev_list; device; device = device->next) + for (device = output_device_list; device; device = device->next) { if (device == check) break; @@ -1304,7 +1301,7 @@ device_add(void *arg, int *retval) cmdarg = arg; add = cmdarg->device; - for (device = dev_list; device; device = device->next) + for (device = output_device_list; device; device = device->next) { if (device->id == add->id) break; @@ -1331,8 +1328,8 @@ device_add(void *arg, int *retval) else device->selected = 0; - device->next = dev_list; - dev_list = device; + device->next = output_device_list; + output_device_list = device; } // Update to a device already in the list else @@ -1392,7 +1389,7 @@ device_remove_family(void *arg, int *retval) cmdarg = arg; remove = cmdarg->device; - for (device = dev_list; device; device = device->next) + for (device = output_device_list; device; device = device->next) { if (device->id == remove->id) break; @@ -1512,7 +1509,7 @@ device_streaming_cb(struct output_device *device, enum output_device_state statu device_remove(device); } else - outputs_device_set_cb(device, device_streaming_cb); + outputs_device_cb_set(device, device_streaming_cb); } static void @@ -1520,7 +1517,7 @@ device_command_cb(struct output_device *device, enum output_device_state status) { DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_command_cb (status %d)\n", outputs_name(device->type), status); - outputs_device_set_cb(device, device_streaming_cb); + outputs_device_cb_set(device, device_streaming_cb); if (status == OUTPUT_STATE_FAILED) device_streaming_cb(device, status); @@ -1622,7 +1619,7 @@ device_activate_cb(struct output_device *device, enum output_device_state status output_sessions++; - outputs_device_set_cb(device, device_streaming_cb); + outputs_device_cb_set(device, device_streaming_cb); out: /* cur_cmd->ret already set @@ -1719,12 +1716,32 @@ device_restart_cb(struct output_device *device, enum output_device_state status) } output_sessions++; - outputs_device_set_cb(device, 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_lost_cb) + return "device_lost_cb"; + else if (p == device_command_cb) + return "device_command_cb"; + else if (p == device_shutdown_cb) + return "device_shutdown_cb"; + else + return "unknown"; +} /* ------------------------- Internal playback routines --------------------- */ @@ -2092,7 +2109,7 @@ playback_start_item(void *arg, int *retval) // 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) { @@ -2110,7 +2127,7 @@ 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; @@ -2434,15 +2451,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) { @@ -2469,7 +2477,7 @@ 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) { @@ -2592,7 +2600,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++) { @@ -2639,7 +2647,7 @@ speaker_enable(void *arg, int *retval) *retval = 0; - for (device = dev_list; device; device = device->next) + for (device = output_device_list; device; device = device->next) { if (*id == device->id) { @@ -2666,7 +2674,7 @@ speaker_disable(void *arg, int *retval) *retval = 0; - for (device = dev_list; device; device = device->next) + for (device = output_device_list; device; device = device->next) { if (*id == device->id) { @@ -2696,7 +2704,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; @@ -2726,7 +2734,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; @@ -2748,7 +2756,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; @@ -2799,7 +2807,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; @@ -2853,7 +2861,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; @@ -3456,7 +3464,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) diff --git a/src/player.h b/src/player.h index 3dc6eee4..525d2f03 100644 --- a/src/player.h +++ b/src/player.h @@ -93,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); @@ -163,6 +160,9 @@ player_raop_verification_kickoff(char **arglist); void player_metadata_send(void *imd, void *omd); +const char * +player_pmap(void *p); + int player_init(void); From 936103f462bef0af2c6fb8eb3495b8f3aa1c9186 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 12 Feb 2019 23:43:54 +0100 Subject: [PATCH 21/86] [outputs] Move device_add/rm to outputs and get rid of advertised flag --- src/outputs.c | 196 ++++++++++++++++++++++++++++--- src/outputs.h | 8 +- src/outputs/alsa.c | 1 - src/outputs/cast.c | 2 - src/outputs/dummy.c | 1 - src/outputs/fifo.c | 1 - src/outputs/pulse.c | 1 - src/outputs/raop.c | 2 - src/player.c | 279 +++++++------------------------------------- 9 files changed, 226 insertions(+), 265 deletions(-) diff --git a/src/outputs.c b/src/outputs.c index 342bfa05..34080528 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -35,6 +35,7 @@ #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 "outputs.h" @@ -122,6 +123,22 @@ callback_remove(struct output_device *device) } } +static void +callback_remove_all(enum output_types type) +{ + struct output_device *device; + + for (device = output_device_list; device; device = device->next) + { + if (type != device->type) + continue; + + outputs_device_cb_set(device, NULL); + + callback_remove(device); + } +} + static int callback_add(struct output_device *device, output_status_cb cb) { @@ -162,22 +179,6 @@ callback_add(struct output_device *device, output_status_cb cb) return callback_id; }; -static void -callback_remove_all(enum output_types type) -{ - struct output_device *device; - - for (device = output_device_list; device; device = device->next) - { - if (type != device->type) - continue; - - outputs_device_cb_set(device, NULL); - - callback_remove(device); - } -} - static void deferred_cb(int fd, short what, void *arg) { @@ -213,6 +214,15 @@ deferred_cb(int fd, short what, void *arg) } } +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) { @@ -338,6 +348,41 @@ buffer_drain(struct output_buffer *obuf) } } +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); +} + + /* ----------------------------------- API ---------------------------------- */ struct output_device * @@ -489,6 +534,125 @@ outputs_listener_notify(void) /* ---------------------------- Called by player ---------------------------- */ +int +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; + + 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(); + + listener_notify(LISTENER_SPEAKER); + + return 0; +} + +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) { diff --git a/src/outputs.h b/src/outputs.h index 9997cfea..ef2fddea 100644 --- a/src/outputs.h +++ b/src/outputs.h @@ -114,7 +114,7 @@ struct output_device // Misc device flags unsigned selected:1; - unsigned advertised:1; +// unsigned advertised:1; unsigned has_password:1; unsigned has_video:1; unsigned requires_auth:1; @@ -266,6 +266,12 @@ outputs_listener_notify(void); /* ---------------------------- Called by player ---------------------------- */ +int +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); diff --git a/src/outputs/alsa.c b/src/outputs/alsa.c index 35a8a46c..a27454d5 100644 --- a/src/outputs/alsa.c +++ b/src/outputs/alsa.c @@ -1028,7 +1028,6 @@ alsa_init(void) device->name = strdup(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); diff --git a/src/outputs/cast.c b/src/outputs/cast.c index b4940740..5323e809 100644 --- a/src/outputs/cast.c +++ b/src/outputs/cast.c @@ -1299,8 +1299,6 @@ cast_device_cb(const char *name, const char *type, const char *domain, const cha DPRINTF(E_INFO, L_CAST, "Adding Chromecast device '%s'\n", name); - device->advertised = 1; - switch (family) { case AF_INET: diff --git a/src/outputs/dummy.c b/src/outputs/dummy.c index 591eb18d..8cf40ad2 100644 --- a/src/outputs/dummy.c +++ b/src/outputs/dummy.c @@ -203,7 +203,6 @@ dummy_init(void) 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); diff --git a/src/outputs/fifo.c b/src/outputs/fifo.c index ccb31e9b..904a1643 100644 --- a/src/outputs/fifo.c +++ b/src/outputs/fifo.c @@ -505,7 +505,6 @@ fifo_init(void) 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); diff --git a/src/outputs/pulse.c b/src/outputs/pulse.c index 2bd455d7..42e9f044 100644 --- a/src/outputs/pulse.c +++ b/src/outputs/pulse.c @@ -428,7 +428,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); diff --git a/src/outputs/raop.c b/src/outputs/raop.c index 512b6578..688cae1f 100644 --- a/src/outputs/raop.c +++ b/src/outputs/raop.c @@ -4712,8 +4712,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: diff --git a/src/player.c b/src/player.c index 6e474fd6..6c028316 100644 --- a/src/player.c +++ b/src/player.c @@ -1203,198 +1203,37 @@ playback_cb(int fd, short what, void *arg) /* ----------------- 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 = 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 -device_remove(struct output_device *remove) -{ - struct output_device *device; - struct output_device *prev; - int ret; - - 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); - - // Make sure device isn't selected anymore - if (remove->selected) - speaker_deselect_output(remove); - - if (!prev) - output_device_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 = output_device_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 = (master_volume >= 0) ? master_volume : PLAYER_DEFAULT_VOLUME; - for (device = output_device_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; + *retval = outputs_device_add(device, new_deselect, default_volume); - 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->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 = output_device_list; - output_device_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 = output_device_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); @@ -1421,16 +1260,16 @@ device_remove_family(void *arg, int *retval) if (!device->v4_address && !device->v6_address) { - device->advertised = 0; + // Make sure device isn't selected anymore + if (device->selected) + speaker_deselect_output(device); - if (!device->session) - device_remove(device); + // Will also stop sessions on the device, if any + outputs_device_remove(device); } outputs_device_free(remove); - listener_notify(LISTENER_SPEAKER); - *retval = 0; return COMMAND_END; } @@ -1471,12 +1310,7 @@ device_metadata_send(void *arg, int *retval) static void 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 (status %d)\n", outputs_name(device->type), status); - - ret = device_check(device); - if (ret < 0) + if (!device) { DPRINTF(E_LOG, L_PLAYER, "Output device disappeared during streaming!\n"); @@ -1484,6 +1318,8 @@ device_streaming_cb(struct output_device *device, enum output_device_state statu 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); @@ -1493,9 +1329,6 @@ device_streaming_cb(struct output_device *device, enum output_device_state statu if (player_state == PLAY_PLAYING) speaker_deselect_output(device); - if (!device->advertised) - device_remove(device); - if (output_sessions == 0) playback_abort(); } @@ -1504,9 +1337,6 @@ device_streaming_cb(struct output_device *device, enum output_device_state statu DPRINTF(E_INFO, L_PLAYER, "The %s device '%s' stopped\n", device->type_name, device->name); output_sessions--; - - if (!device->advertised) - device_remove(device); } else outputs_device_cb_set(device, device_streaming_cb); @@ -1515,6 +1345,12 @@ device_streaming_cb(struct output_device *device, enum output_device_state statu static void device_command_cb(struct output_device *device, enum output_device_state status) { + if (!device) + { + DPRINTF(E_LOG, L_PLAYER, "Output device disappeared before command completion!\n"); + goto out; + } + 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); @@ -1530,6 +1366,7 @@ device_command_cb(struct output_device *device, enum output_device_state status) input_buffer_full_cb(player_playback_start); } + out: commands_exec_end(cmdbase, 0); } @@ -1537,16 +1374,12 @@ static void 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 (status %d)\n", outputs_name(device->type), status); 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"); @@ -1555,8 +1388,7 @@ device_shutdown_cb(struct output_device *device, enum output_device_state status goto out; } - 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 @@ -1566,39 +1398,23 @@ device_shutdown_cb(struct output_device *device, enum output_device_state status commands_exec_end(cmdbase, retval); } -static void -device_lost_cb(struct output_device *device, enum output_device_state status) -{ - DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_lost_cb (status %d)\n", outputs_name(device->type), status); - - // 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, enum output_device_state status) { int retval; - int ret; - - DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_activate_cb (status %d)\n", outputs_name(device->type), status); 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_device_stop(device, device_lost_cb); - 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; @@ -1609,9 +1425,6 @@ device_activate_cb(struct output_device *device, enum output_device_state status { speaker_deselect_output(device); - if (!device->advertised) - device_remove(device); - if (retval != -2) retval = -1; goto out; @@ -1634,13 +1447,9 @@ static void 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 (status %d)\n", outputs_name(device->type), status); 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"); @@ -1649,6 +1458,8 @@ device_probe_cb(struct output_device *device, enum output_device_state status) 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; @@ -1659,9 +1470,6 @@ device_probe_cb(struct output_device *device, enum output_device_state status) { speaker_deselect_output(device); - if (!device->advertised) - device_remove(device); - if (retval != -2) retval = -1; goto out; @@ -1680,23 +1488,19 @@ static void 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 (status %d)\n", outputs_name(device->type), status); 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_device_stop(device, device_lost_cb); - 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; @@ -1707,9 +1511,6 @@ device_restart_cb(struct output_device *device, enum output_device_state status) { speaker_deselect_output(device); - if (!device->advertised) - device_remove(device); - if (retval != -2) retval = -1; goto out; @@ -1733,8 +1534,6 @@ player_pmap(void *p) return "device_activate_cb"; else if (p == device_streaming_cb) return "device_streaming_cb"; - else if (p == device_lost_cb) - return "device_lost_cb"; else if (p == device_command_cb) return "device_command_cb"; else if (p == device_shutdown_cb) @@ -2479,7 +2278,7 @@ speaker_enumerate(void *arg, int *retval) for (device = output_device_list; device; device = device->next) { - if (device->advertised || device->selected) + if (device->selected) { device_to_speaker_info(&spk, device); spk_enum->cb(&spk, spk_enum->arg); From e97ad7d9702f9456963242be9bf2f7888a7fd6fe Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Wed, 13 Feb 2019 16:56:17 +0100 Subject: [PATCH 22/86] [outputs] Reinstate device->activated, so that mdns flukes can't bring down playback --- src/outputs.c | 11 +++++++++++ src/outputs.h | 2 +- src/player.c | 52 +++++++++++++++++++++++---------------------------- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/src/outputs.c b/src/outputs.c index 34080528..0fe27e24 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -103,6 +103,7 @@ struct output_quality_subscription static struct output_quality_subscription output_quality_subscriptions[OUTPUTS_MAX_QUALITY_SUBSCRIPTIONS + 1]; static bool output_got_new_subscription; + /* ------------------------------- MISC HELPERS ----------------------------- */ static void @@ -201,6 +202,14 @@ deferred_cb(int fd, short what, void *arg) memset(&outputs_cb_queue[callback_id], 0, sizeof(struct outputs_callback_queue)); + // 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); @@ -606,6 +615,8 @@ outputs_device_add(struct output_device *add, bool new_deselect, int default_vol device_list_sort(); + device->advertised = 1; + listener_notify(LISTENER_SPEAKER); return 0; diff --git a/src/outputs.h b/src/outputs.h index ef2fddea..e3802e78 100644 --- a/src/outputs.h +++ b/src/outputs.h @@ -114,7 +114,7 @@ struct output_device // Misc device flags unsigned selected:1; -// unsigned advertised:1; + unsigned advertised:1; unsigned has_password:1; unsigned has_video:1; unsigned requires_auth:1; diff --git a/src/player.c b/src/player.c index 6c028316..c5b11300 100644 --- a/src/player.c +++ b/src/player.c @@ -1211,6 +1211,7 @@ device_add(void *arg, int *retval) bool new_deselect; int default_volume; + // Default volume for new devices default_volume = (master_volume >= 0) ? master_volume : PLAYER_DEFAULT_VOLUME; // Never turn on new devices during playback @@ -1260,12 +1261,19 @@ device_remove_family(void *arg, int *retval) if (!device->v4_address && !device->v6_address) { - // Make sure device isn't selected anymore - if (device->selected) - speaker_deselect_output(device); + device->advertised = 0; - // Will also stop sessions on the device, if any - outputs_device_remove(device); + // 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) + { + if (device->selected) + speaker_deselect_output(device); + + outputs_device_remove(device); + } } outputs_device_free(remove); @@ -2440,20 +2448,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 = output_device_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 @@ -2467,20 +2468,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 = output_device_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 From 87ca6363aebfb0f000b3ae824bca6d53bbeb52d8 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 16 Feb 2019 19:34:36 +0100 Subject: [PATCH 23/86] [player/input] Refactor - WIP * Open input sources earlier * Gapless playback * Remove fixed 44100/16 from player * Complete restructure player internals --- src/httpd_artworkapi.c | 2 +- src/httpd_dacp.c | 4 +- src/input.c | 693 ++++++++++++------- src/input.h | 158 ++--- src/inputs/file_http.c | 120 ++-- src/inputs/pipe.c | 146 ++-- src/inputs/spotify.c | 33 +- src/misc.h | 5 + src/outputs/raop.c | 6 +- src/player.c | 1458 ++++++++++++++++++---------------------- src/player.h | 2 +- src/spotify.c | 3 +- 12 files changed, 1315 insertions(+), 1315 deletions(-) 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/input.c b/src/input.c index 42173fba..2b633fa0 100644 --- a/src/input.c +++ b/src/input.c @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -37,6 +38,7 @@ #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 @@ -100,10 +102,26 @@ struct input_buffer 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 *inputev; +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; @@ -115,7 +133,7 @@ static size_t debug_elapsed; #endif -/* ------------------------------ MISC HELPERS ---------------------------- */ +/* ------------------------------- MISC HELPERS ----------------------------- */ static int map_data_kind(int data_kind) @@ -142,14 +160,14 @@ map_data_kind(int data_kind) } static void -marker_add(short flags) +marker_add(size_t pos, short flags) { struct marker *head; struct marker *marker; CHECK_NULL(L_PLAYER, marker = calloc(1, sizeof(struct marker))); - marker->pos = input_buffer.bytes_written; + marker->pos = pos; marker->quality = input_buffer.cur_write_quality; marker->flags = flags; @@ -162,88 +180,280 @@ marker_add(short flags) head->prev = marker; } -static int -source_check_and_map(struct player_source *ps, const char *action, char check_setup) + +/* ------------------------- INPUT SOURCE HANDLING -------------------------- */ + +static void +clear(struct input_source *source) { - int type; - -#ifdef DEBUG - DPRINTF(E_DBG, L_PLAYER, "Action is %s\n", action); -#endif - - if (!ps) - { - DPRINTF(E_LOG, L_PLAYER, "Stream %s called with invalid player source\n", action); - return -1; - } - - if (check_setup && !ps->setup_done) - { - DPRINTF(E_LOG, L_PLAYER, "Given player source not setup, %s not possible\n", action); - return -1; - } - - type = map_data_kind(ps->data_kind); - if (type < 0) - { - DPRINTF(E_LOG, L_PLAYER, "Unsupported input type, %s not possible\n", action); - return -1; - } - - return type; + free(source->path); + memset(source, 0, sizeof(struct input_source)); } -/* ----------------------------- PLAYBACK LOOP ---------------------------- */ -/* Thread: input */ - -// TODO Thread safety of ps? -static void * -playback(void *arg) +static void +flush(short *flags) { - struct player_source *ps = arg; - int type; - int ret; - - type = source_check_and_map(ps, "start", 1); - if ((type < 0) || (inputs[type]->disabled)) - goto thread_exit; - - // Loops until input_loop_break is set or no more input, e.g. EOF - ret = inputs[type]->start(ps); - if (ret < 0) - input_write(NULL, NULL, INPUT_FLAG_ERROR); - -#ifdef DEBUG - DPRINTF(E_DBG, L_PLAYER, "Playback loop stopped (break is %d, ret %d)\n", input_loop_break, ret); -#endif - - thread_exit: - pthread_exit(NULL); -} - -void -input_wait(void) -{ - struct timespec ts; + 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->flags; + input_buffer.marker_tail = marker->prev; + 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 + DPRINTF(E_DBG, L_PLAYER, "Flushing %zu bytes with flags %d\n", len, *flags); +#endif } +static enum command_state +stop(void *arg, int *retval) +{ + short flags; + int type; + + event_del(inputev); + + type = input_now_reading.type; + + if (inputs[type]->stop) + inputs[type]->stop(&input_now_reading); + + flush(&flags); + + clear(&input_now_reading); + + *retval = 0; + return COMMAND_END; +} + +static int +seek(struct input_source *source, int seek_ms) +{ + if (seek_ms > 0 && 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; + + ret = seek(source, seek_ms); + if (ret < 0) + goto seek_error; + + return ret; + + seek_error: + stop(NULL, NULL); + 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(NULL, NULL); + + // 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_active(inputev, 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 +next(void *arg, int *retval) +{ + struct player_status status; + struct db_queue_item *queue_item; + uint32_t item_id; + int type; + int ret; + + // We may have finished reading source way before end of playback, and we + // don't want to proceed prematurely. So we wait until the input buffer is + // below the write threshold. + ret = input_wait(); + if (ret < 0) + { + input_next(); // Async call to ourselves + return; + } + + item_id = input_now_reading.item_id; + + // Cleans up the source that has ended/failed and clears input_now_reading + stop(NULL, NULL); + + player_get_status(&status); + + // TODO what about repeat/repeat_all? Maybe move next() to player that can + // just call input_start() + + // Get the next queue_item from the db + queue_item = db_queue_fetch_next(item_id, status.shuffle); + if (!queue_item) + { + DPRINTF(E_DBG, L_PLAYER, "Reached end of playback queue\n"); + *retval = 0; + return COMMAND_END; + } + + ret = setup(&input_now_reading, queue_item, 0); + free_queue_item(queue_item, 0); + if (ret < 0) + goto error; + + DPRINTF(E_DBG, L_PLAYER, "Continuing input read loop for item '%s' (item id %" PRIu32 ")\n", input_now_reading.path, input_now_reading.item_id); + + event_active(inputev, 0, 0); + + *retval = 0; + return COMMAND_END; + + error: + input_write(NULL, NULL, INPUT_FLAG_ERROR); + clear(&input_now_reading); + *retval = -1; + return COMMAND_END; +} +*/ + +static enum command_state +metadata_get(void *arg, int *retval) +{ + struct input_arg *cmdarg = arg; + int type; + + if (!input_now_reading.open) + { + DPRINTF(E_WARN, L_PLAYER, "Source is no longer available for input_metadata_get()\n"); + goto error; + } + + type = input_now_reading.type; + if ((type < 0) || (inputs[type]->disabled)) + goto error; + + if (inputs[type]->metadata_get) + *retval = inputs[type]->metadata_get(cmdarg->metadata, &input_now_reading); + else + *retval = 0; + + return COMMAND_END; + + error: + *retval = -1; + return COMMAND_END; +} + + +/* ---------------------- Interface towards input backends ------------------ */ +/* Thread: input and spotify */ + // Called by input modules from within the playback loop int input_write(struct evbuffer *evbuf, struct media_quality *quality, short flags) { - struct timespec ts; + bool read_end; 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 ((evbuffer_get_length(input_buffer.evbuf) > INPUT_BUFFER_THRESHOLD) && evbuf) { if (input_buffer.full_cb) { @@ -251,26 +461,19 @@ input_write(struct evbuffer *evbuf, struct media_quality *quality, 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) - { - pthread_mutex_unlock(&input_buffer.mutex); - return 0; } if (quality && !quality_is_equal(quality, &input_buffer.cur_write_quality)) { input_buffer.cur_write_quality = *quality; - marker_add(INPUT_FLAG_QUALITY); + marker_add(input_buffer.bytes_written, INPUT_FLAG_QUALITY); } ret = 0; @@ -280,24 +483,109 @@ input_write(struct evbuffer *evbuf, struct media_quality *quality, short flags) ret = evbuffer_add_buffer(input_buffer.evbuf, evbuf); if (ret < 0) { - DPRINTF(E_LOG, L_PLAYER, "Error adding stream data to input buffer\n"); + DPRINTF(E_LOG, L_PLAYER, "Error adding stream data to input buffer, stopping\n"); + input_stop(); flags |= INPUT_FLAG_ERROR; } } - // Note this marker is added at the post-write position, since EOF and ERROR - // belong there. We never want to add a marker for the NONBLOCK flag. - if (flags & ~INPUT_FLAG_NONBLOCK) - marker_add(flags); + if (flags) + { + if (input_buffer.bytes_written > INPUT_BUFFER_THRESHOLD) + marker_add(input_buffer.bytes_written - INPUT_BUFFER_THRESHOLD, INPUT_FLAG_START_NEXT); + else + marker_add(input_buffer.bytes_written, INPUT_FLAG_START_NEXT); + + // Note this marker is added at the post-write position, since EOF, error + // and metadata belong there. + marker_add(input_buffer.bytes_written, flags); + } pthread_mutex_unlock(&input_buffer.mutex); return ret; } +int +input_wait(void) +{ + struct timespec ts; -/* -------------------- Interface towards player thread ------------------- */ -/* Thread: player */ + pthread_mutex_lock(&input_buffer.mutex); + + ts = timespec_reltoabs(input_loop_timeout); + pthread_cond_timedwait(&input_buffer.cond, &input_buffer.mutex, &ts); + + // Is the buffer full? + 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 inputev + 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) + return; // Error or EOF, so don't come back + + event_add(inputev, &tv); +} + + +/* ---------------------- Interface towards player thread ------------------- */ +/* Thread: player */ int input_read(void *data, size_t size, short *flags) @@ -307,12 +595,6 @@ input_read(void *data, size_t size, short *flags) *flags = 0; - if (!tid_input) - { - DPRINTF(E_LOG, L_PLAYER, "Bug! Read called, but playback not running\n"); - return -1; - } - pthread_mutex_lock(&input_buffer.mutex); // First we check if there is a marker in the requested samples. If there is, @@ -380,157 +662,40 @@ 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, NULL); } void input_flush(short *flags) { - struct marker *marker; - size_t len; - - pthread_mutex_lock(&input_buffer.mutex); - - // 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->flags; - input_buffer.marker_tail = marker->prev; - 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 - DPRINTF(E_DBG, L_PLAYER, "Flushing %zu bytes with flags %d\n", len, *flags); -#endif + // Flush should be thread safe + flush(flags); } int @@ -542,33 +707,13 @@ input_quality_get(struct media_quality *quality) } int -input_metadata_get(struct input_metadata *metadata, struct player_source *ps, int startup, uint64_t rtptime) +input_metadata_get(struct input_metadata *metadata) { - int type; + struct input_arg cmdarg; - if (!metadata || !ps || !ps->stream_start || !ps->output_start) - { - DPRINTF(E_LOG, L_PLAYER, "Bug! Unhandled case in input_metadata_get()\n"); - return -1; - } + cmdarg.metadata = metadata; - 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); + return commands_exec_sync(cmdbase, metadata_get, NULL, &cmdarg); } void @@ -597,12 +742,9 @@ 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, inputev = event_new(evbase_input, -1, EV_PERSIST, play, NULL)); no_input = 1; for (i = 0; inputs[i]; i++) @@ -610,7 +752,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) @@ -627,17 +769,42 @@ 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: + event_free(inputev); + 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++) { @@ -648,9 +815,21 @@ 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); + event_free(inputev); evbuffer_free(input_buffer.evbuf); + event_base_free(evbase_input); } diff --git a/src/input.h b/src/input.h index aa7120cf..e3797366 100644 --- a/src/input.h +++ b/src/input.h @@ -6,8 +6,8 @@ # include #endif #include +#include "db.h" #include "misc.h" -#include "transcode.h" // Must be in sync with inputs[] in input.c enum input_types @@ -22,64 +22,49 @@ 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), + 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); @@ -90,6 +75,7 @@ struct input_metadata int startup; + uint64_t start; uint64_t rtptime; uint64_t offset; @@ -115,55 +101,63 @@ 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. Data must be PCM-LE * samples. 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). + * copying memory. * * @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, 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. @@ -186,39 +180,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(). @@ -236,7 +222,7 @@ input_quality_get(struct media_quality *quality); * 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); +input_metadata_get(struct input_metadata *metadata); /* * Free the entire struct diff --git a/src/inputs/file_http.c b/src/inputs/file_http.c index 4f2743f1..4c435722 100644 --- a/src/inputs/file_http.c +++ b/src/inputs/file_http.c @@ -30,96 +30,100 @@ #include "input.h" static int -setup(struct player_source *ps) +setup(struct input_source *source) { - ps->input_ctx = transcode_setup(XCODE_PCM_NATIVE, ps->data_kind, ps->path, ps->len_ms, NULL); - if (!ps->input_ctx) + struct transcode_ctx *ctx; + + ctx = transcode_setup(XCODE_PCM_NATIVE, 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 transcode_ctx *ctx = ps->input_ctx; - struct media_quality quality = { 0 }; - struct evbuffer *evbuf; - short flags; - int ret; - int icy_timer; - - evbuf = evbuffer_new(); - - quality.sample_rate = transcode_encode_query(ctx->encode_ctx, "sample_rate"); - quality.bits_per_sample = transcode_encode_query(ctx->encode_ctx, "bits_per_sample"); - quality.channels = transcode_encode_query(ctx->encode_ctx, "channels"); - - 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, ctx, 1); - if (ret < 0) - break; - - flags = ((ret == 0) ? INPUT_FLAG_EOF : 0) | - (icy_timer ? INPUT_FLAG_METADATA : 0); - - ret = input_write(evbuf, &quality, 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); + evbuffer_free(source->evbuf); - ps->input_ctx = NULL; - ps->setup_done = 0; + source->input_ctx = 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; @@ -147,7 +151,7 @@ struct input_definition input_file = .type = INPUT_TYPE_FILE, .disabled = 0, .setup = setup, - .start = start, + .play = play, .stop = stop, .seek = seek, }; @@ -158,7 +162,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 b37308bc..a13dced1 100644 --- a/src/inputs/pipe.c +++ b/src/inputs/pipe.c @@ -125,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) @@ -500,8 +500,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 @@ -617,8 +617,8 @@ pipe_thread_run(void *arg) } -/* -------------------------- METADATA PIPE HANDLING ---------------------- */ -/* Thread: worker */ +/* --------------------------- METADATA PIPE HANDLING ----------------------- */ +/* Thread: worker */ static void pipe_metadata_watch_del(void *arg) @@ -706,8 +706,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) @@ -802,92 +802,46 @@ 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 media_quality quality = { 0 }; - 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; - } - - quality.sample_rate = pipe_sample_rate; - quality.bits_per_sample = pipe_bits_per_sample; - quality.channels = 2; - - ret = -1; - while (!input_loop_break) - { - ret = evbuffer_read(evbuf, pipe->fd, PIPE_READ_MAX); - if ((ret == 0) && (pipe->is_autostarted)) - { - input_write(evbuf, NULL, 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, &quality, 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"); + evbuffer_free(source->evbuf); + pipe_close(pipe->fd); // Reset the pipe and start watching it again for new data. Must be async or @@ -903,14 +857,51 @@ stop(struct player_source *ps) pipe_free(pipe); - ps->input_ctx = NULL; - ps->setup_done = 0; + source->input_ctx = 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); @@ -927,8 +918,9 @@ metadata_get(struct input_metadata *metadata, struct player_source *ps, uint64_t if (pipe_metadata_parsed.song_length) { - if (rtptime > ps->stream_start) - metadata->rtptime = rtptime - pipe_metadata_parsed.offset; +// TODO this is probably broken + if (metadata->rtptime > metadata->start) + metadata->rtptime -= pipe_metadata_parsed.offset; metadata->offset = pipe_metadata_parsed.offset; metadata->song_length = pipe_metadata_parsed.song_length; } @@ -988,7 +980,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..cddc1424 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; diff --git a/src/misc.h b/src/misc.h index 2921cfa4..dcd0396c 100644 --- a/src/misc.h +++ b/src/misc.h @@ -21,6 +21,11 @@ # 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; diff --git a/src/outputs/raop.c b/src/outputs/raop.c index 688cae1f..0dd347f3 100644 --- a/src/outputs/raop.c +++ b/src/outputs/raop.c @@ -2358,6 +2358,9 @@ raop_metadata_send_progress(struct raop_session *rs, struct evbuffer *evbuf, str return -1; } + DPRINTF(E_DBG, L_PLAYER, "Metadata send is start_time=%zu, start=%zu, display=%zu, current=%zu, end=%zu\n", + rs->start_rtptime, rmd->start, rmd->start - delay, rmd->start + offset, rmd->end); + ret = raop_send_req_set_parameter(rs, evbuf, "text/parameters", NULL, raop_cb_metadata, "send_progress"); if (ret < 0) DPRINTF(E_LOG, L_RAOP, "Could not send SET_PARAMETER progress request to '%s'\n", rs->devname); @@ -3014,7 +3017,8 @@ packets_sync_send(struct raop_master_session *rms, struct timespec pts) // 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. - cur_stamp.ts.tv_sec = pts.tv_sec - OUTPUTS_BUFFER_DURATION; +// TODO update comment to match reality + cur_stamp.ts.tv_sec = pts.tv_sec; cur_stamp.ts.tv_nsec = pts.tv_nsec; // The cur_pos will be the rtptime of the coming packet, minus diff --git a/src/player.c b/src/player.c index c5b11300..a42cc9da 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 @@ -101,14 +101,6 @@ # 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 @@ -134,9 +126,6 @@ // (value is in milliseconds) #define PLAYER_WRITE_BEHIND_MAX 1500 -// TODO get rid of me -#define TEMP_NEXT_RTPTIME (last_rtptime + pb_session.samples_written + pb_session.samples_per_read) - struct volume_param { int volume; uint64_t spk_id; @@ -185,25 +174,78 @@ 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; - uint32_t samples_written; - int samples_per_read; - - struct media_quality quality; - // The time the playback session started struct timespec start_ts; // The time the first sample in the buffer should be played by the output. // It will be equal to: // pts = start_ts + OUTPUTS_BUFFER_DURATION + ticks_elapsed * player_tick_interval +// TODO is above still correct? struct timespec pts; + + // Equals current number of samples written to outputs + uint32_t pos; + + // 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 int debug_counter = -1; + static struct player_session pb_session; struct event_base *evbase_player; @@ -250,8 +292,8 @@ static int pb_write_deficit_max; static bool pb_write_recovery; // Sync values -static struct timespec pb_pos_stamp; -static uint64_t pb_pos; +//static struct timespec pb_pos_stamp; +//static uint64_t pb_pos; // Stream position (packets) static uint64_t last_rtptime; @@ -263,8 +305,6 @@ 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; @@ -284,8 +324,6 @@ playback_abort(void); static void playback_suspend(void); -static int -player_get_current_pos(uint64_t *pos, struct timespec *ts, int commit); /* ----------------------------- Volume helpers ----------------------------- */ @@ -417,6 +455,12 @@ metadata_update_cb(void *arg) struct db_queue_item *queue_item; int ret; + ret = input_metadata_get(metadata); + if (ret < 0) + { + goto out_free_metadata; + } + queue_item = db_queue_fetch_byitemid(metadata->item_id); if (!queue_item) { @@ -470,11 +514,15 @@ static void metadata_trigger(int startup) { struct input_metadata metadata; - int ret; - ret = input_metadata_get(&metadata, cur_streaming, startup, TEMP_NEXT_RTPTIME); - if (ret < 0) - return; + memset(&metadata, 0, sizeof(struct input_metadata)); + + metadata.item_id = pb_session.playing_now->item_id; + + metadata.startup = startup; + metadata.start = pb_session.playing_now->read_start; + metadata.offset = pb_session.playing_now->play_start - pb_session.playing_now->read_start; + metadata.rtptime = pb_session.pos; worker_execute(metadata_update_cb, &metadata, sizeof(metadata), 0); } @@ -511,16 +559,57 @@ 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) + return NULL; } + 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) + return NULL; + } + } + + if (!queue_item) + { + DPRINTF(E_DBG, L_PLAYER, "Reached end of queue\n"); + return NULL; + } + + return queue_item; +} + +static struct db_queue_item * +queue_item_prev(uint32_t item_id) +{ + return db_queue_fetch_prev(item_id, shuffle); } static void @@ -534,15 +623,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 */ @@ -551,559 +631,442 @@ 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 = TEMP_NEXT_RTPTIME - ((uint64_t)ret * 44100) / 1000; - cur_streaming->output_start = TEMP_NEXT_RTPTIME; - 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 = TEMP_NEXT_RTPTIME - ((uint64_t)ret * 44100) / 1000; - cur_streaming->output_start = TEMP_NEXT_RTPTIME; - - 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; + struct db_queue_item *queue_item; - ret = player_get_current_pos(&pos, &ts, 0); - if (ret < 0) + if (!current) + return NULL; + + queue_item = queue_item_next(current->item_id); + if (!queue_item) { - DPRINTF(E_LOG, L_PLAYER, "Couldn't get current playback position\n"); - - return 0; + DPRINTF(E_LOG, L_PLAYER, "Error fetching next item from queue (item-id=%d, repeat=%d)\n", current->item_id, repeat); + return NULL; } - if (player_state == PLAY_STOPPED) + ps = source_new(queue_item); + + free_queue_item(queue_item, 0); + + return ps; +} + +static void +source_next(void) +{ + if (!pb_session.reading_next) + return; + + DPRINTF(E_DBG, L_PLAYER, "Opening next track: '%s' (id=%d)\n", pb_session.reading_next->path, pb_session.reading_next->item_id); + + input_start(pb_session.reading_next->item_id); +} + +static int +source_start(void) +{ + if (!pb_session.reading_next) + return 0; + + 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); + + return input_seek(pb_session.reading_next->item_id, (int)pb_session.reading_next->seek_ms); +} + + +/* ------------------------ 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. + +static int +source_print(char *line, size_t linesize, struct player_source *ps, const char *name) +{ + int pos = 0; + + if (ps) { - 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); + 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); 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() +static void +session_dump(void) { - struct player_source *ps = NULL; - struct db_queue_item *queue_item; + char line[4096]; + int pos = 0; - if (!cur_streaming) - { - DPRINTF(E_LOG, L_PLAYER, "source_next() called with no current streaming source available\n"); - return NULL; - } + 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"); - 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); - } + DPRINTF(E_DBG, L_PLAYER, "%s\n", line); - 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; - } - } - } + 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"); - if (!queue_item) - { - DPRINTF(E_DBG, L_PLAYER, "Reached end of queue\n"); - return NULL; - } + DPRINTF(E_DBG, L_PLAYER, "%s\n", line); - ps = source_new(queue_item); - free_queue_item(queue_item, 0); - return ps; + 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); } -/* - * Returns the previous player source based on the current streaming source - */ -static struct player_source * -source_prev() +static void +session_update_play_eof(void) { - 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); - if (!queue_item) - return NULL; - - ps = source_new(queue_item); - free_queue_item(queue_item, 0); - - return ps; + pb_session.playing_now = pb_session.reading_now; } -static int -source_switch(int nbytes) +static void +session_update_play_start(void) +{ + pb_session.playing_now = pb_session.reading_now; +} + +static void +session_update_read_next(void) { struct player_source *ps; - int ret; - DPRINTF(E_DBG, L_PLAYER, "Switching track\n"); - - source_close(TEMP_NEXT_RTPTIME + BTOS(nbytes, pb_session.quality.bits_per_sample, 2) - 1); - - while ((ps = source_next())) - { - ret = source_open(ps, cur_streaming->end + 1, 0); - if (ret < 0) - { - db_queue_delete_byitemid(ps->item_id); - continue; - } - - ret = source_play(); - if (ret < 0) - { - db_queue_delete_byitemid(ps->item_id); - source_close(TEMP_NEXT_RTPTIME + BTOS(nbytes, pb_session.quality.bits_per_sample, 2) - 1); - continue; - } - - break; - } - - if (!ps) // End of queue - { - cur_streaming = NULL; - return 0; - } - - metadata_trigger(0); - - return 0; + ps = source_next_create(pb_session.reading_next); + source_free(&pb_session.reading_next); + pb_session.reading_next = ps; } static void -session_reset(struct player_session *session, struct media_quality *quality) +session_update_read_eof(void) { - session->samples_written = 0; - session->quality = *quality; - session->samples_per_read = (quality->sample_rate / 1000) * (player_tick_interval.tv_nsec / 1000000); - session->bufsize = STOB(session->samples_per_read, quality->bits_per_sample, quality->channels); + pb_session.reading_now->read_end = pb_session.pos - 1; + pb_session.reading_now->play_end = pb_session.pos - 1 + 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_session.reading_now->read_start = pb_session.pos; +} + +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->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.pos == 0) + { + 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.reading_now->quality)) + return; + + 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); 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, - session->samples_per_read, session->bufsize); + quality->sample_rate, quality->bits_per_sample, quality->channels, samples_per_read, pb_session.bufsize); - if (session->buffer) - session->buffer = realloc(session->buffer, session->bufsize); + if (pb_session.buffer) + pb_session.buffer = realloc(pb_session.buffer, pb_session.bufsize); else - session->buffer = malloc(session->bufsize); + pb_session.buffer = malloc(pb_session.bufsize); - CHECK_NULL(L_PLAYER, session->buffer); + CHECK_NULL(L_PLAYER, pb_session.buffer); - clock_gettime_with_res(CLOCK_MONOTONIC, &session->start_ts, &player_timer_res); - session->pts.tv_sec = session->start_ts.tv_sec + OUTPUTS_BUFFER_DURATION; - session->pts.tv_nsec = session->start_ts.tv_nsec; + pb_session.reading_now->play_start = pb_session.reading_now->play_start + pb_session.reading_now->output_buffer_samples; } static void -session_clear(struct player_session *session) +session_stop(void) { - free(session->buffer); - memset(session, 0, sizeof(struct player_session)); + 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)); } -/* ----------------- Main read, write and playback timer event -------------- */ - -// Returns -1 on error (caller should abort playback), or bytes read (possibly 0) -static int -source_read(uint8_t *buf, int len) +static void +session_start(struct player_source *ps, uint32_t seek_ms) { - struct media_quality quality; - int nbytes; - uint32_t item_id; - int ret; - short flags; + session_stop(); - // Nothing to read, stream silence until source_check() stops playback - if (!cur_streaming) + // 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() +{ + DPRINTF(E_DBG, L_PLAYER, "event_read_quality()\n"); + + struct media_quality quality; + + input_quality_get(&quality); + + 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), session is not affected by +// this, so there is no session update +static void +event_read_start_next() +{ + DPRINTF(E_DBG, L_PLAYER, "event_start_next()\n"); + + source_next(); +} + +static void +event_metadata_new() +{ + DPRINTF(E_DBG, L_PLAYER, "event_metadata_new()\n"); + + metadata_trigger(0); +} + +static void +event_play_end() +{ + DPRINTF(E_DBG, L_PLAYER, "event_play_end()\n"); + + playback_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) { - memset(buf, 0, len); - return len; + 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); } - 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); + if (consume) + db_queue_delete_byitemid(pb_session.playing_now->item_id); - nbytes = 0; - item_id = cur_streaming->item_id; - ret = source_switch(0); - db_queue_delete_byitemid(item_id); - if (ret < 0) - return -1; + outputs_metadata_prune(pb_session.pos); + + session_update_play_eof(); +} + +static void +event_play_start() +{ + DPRINTF(E_DBG, L_PLAYER, "event_play_start()\n"); + + event_metadata_new(); + + status_update(PLAY_PLAYING); + + session_update_play_start(); +} + +// 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) +{ + if (!pb_session.playing_now) // Shouldn't happen + 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) + { + event_play_end(); + return; + } + } + + if (pb_session.playing_now->pos_ms == 0 && 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, struct media_quality *quality, uint8_t *buf, int len) +{ + short flags; + + *quality = pb_session.reading_now->quality; + *nsamples = 0; + *nbytes = input_read(buf, len, &flags); + if ((*nbytes < 0) || (flags & 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 (flags & INPUT_FLAG_START_NEXT) + { + event_read_start_next(); } else if (flags & INPUT_FLAG_EOF) { - ret = source_switch(nbytes); - if (ret < 0) - return -1; + event_read_eof(); } else if (flags & INPUT_FLAG_METADATA) { - metadata_trigger(0); + event_metadata_new(); } else if (flags & INPUT_FLAG_QUALITY) { - input_quality_get(&quality); - - if (!quality_is_equal(&quality, &pb_session.quality)) - session_reset(&pb_session, &quality); + event_read_quality(); } - // 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; - } + if (*nbytes == 0 || quality->channels == 0) + return 0; - return nbytes; + *nsamples = BTOS(*nbytes, quality->bits_per_sample, quality->channels); + + event_read(*nsamples); + + return 0; } static void playback_cb(int fd, short what, void *arg) { struct timespec ts; + struct media_quality quality; uint64_t overrun; - int got; + int nbytes; int nsamples; int i; int ret; @@ -1147,40 +1110,35 @@ playback_cb(int fd, short what, void *arg) pb_write_recovery = false; } +// debug_counter++; +// if (debug_counter % 100 == 0) +// session_dump(); + // 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. - for (i = 1 + overrun + pb_read_deficit; i > 0; i--) + for (i = 1 + overrun + pb_read_deficit; i > 0 && pb_session.reading_now; i--) { - source_check(); - - // Make sure playback is still running after source_check() - if (player_state != PLAY_PLAYING) - return; - - got = source_read(pb_session.buffer, pb_session.bufsize); - if (got < 0) + ret = source_read(&nbytes, &nsamples, &quality, pb_session.buffer, pb_session.bufsize); + if (ret < 0) { - DPRINTF(E_LOG, L_PLAYER, "Error reading from source, aborting playback\n"); - playback_abort(); + DPRINTF(E_LOG, L_PLAYER, "Error reading from source\n"); break; } - else if (got == 0) + if (nbytes == 0) { pb_read_deficit++; break; } - nsamples = BTOS(got, pb_session.quality.bits_per_sample, pb_session.quality.channels); - outputs_write(pb_session.buffer, pb_session.bufsize, &pb_session.quality, nsamples, &pb_session.pts); - pb_session.samples_written += nsamples; + outputs_write(pb_session.buffer, pb_session.bufsize, &quality, nsamples, &pb_session.pts); - if (got < pb_session.bufsize) + if (nbytes < pb_session.bufsize) { - DPRINTF(E_DBG, L_PLAYER, "Incomplete read, wanted %zu, got %d\n", pb_session.bufsize, got); + DPRINTF(E_DBG, L_PLAYER, "Incomplete read, wanted %zu, got %d\n", pb_session.bufsize, nbytes); // How much the number of samples we got corresponds to in time (nanoseconds) ts.tv_sec = 0; - ts.tv_nsec = 1000000000L * nsamples / pb_session.quality.sample_rate; + ts.tv_nsec = 1000000000L * nsamples / quality.sample_rate; pb_session.pts = timespec_add(pb_session.pts, ts); pb_read_deficit++; } @@ -1608,32 +1566,57 @@ playback_timer_stop(void) return 0; } +// Initiates the session and starts the input source static int -pb_session_stop(void) +playback_session_start(struct db_queue_item *queue_item, uint32_t seek_ms) { + struct player_source *ps; int ret; - ret = playback_timer_stop(); + ps = source_new(queue_item); - session_clear(&pb_session); + // 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, remove it from queue and try next in line + db_queue_delete_byitemid(pb_session.reading_next->item_id); + session_update_read_next(); + } + + session_update_read_start((uint32_t)ret); + + if (!pb_session.playing_now) + return -1; return ret; } +// Stops input source and deallocates pb_session content +static void +playback_session_stop(void) +{ + source_stop(); + + session_stop(); + + playback_timer_stop(); + + status_update(PLAY_STOPPED); +} + static void playback_abort(void) { outputs_playback_stop(); - pb_session_stop(); - - source_stop(); + playback_session_stop(); if (!clear_queue_on_stop_disabled) db_queue_clear(0); - status_update(PLAY_STOPPED); - outputs_metadata_purge(); } @@ -1644,7 +1627,7 @@ playback_suspend(void) { player_flush_pending = outputs_flush(device_command_cb); - pb_session_stop(); + playback_timer_stop(); status_update(PLAY_PAUSED); @@ -1662,10 +1645,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)); @@ -1682,59 +1661,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 = TEMP_NEXT_RTPTIME - 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; } @@ -1744,21 +1704,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; } @@ -1766,27 +1723,20 @@ 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); - pb_session_stop(); - - ps_playing = source_now_playing(); - if (ps_playing) - { - history_add(ps_playing->id, ps_playing->item_id); - } - - source_stop(); + playback_session_stop(); status_update(PLAY_STOPPED); @@ -1804,29 +1754,19 @@ playback_start_bh(void *arg, int *retval) { int ret; - // 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; + goto error; status_update(PLAY_PLAYING); *retval = 0; return COMMAND_END; - out_fail: + error: playback_abort(); - *retval = -1; return COMMAND_END; } @@ -1838,7 +1778,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) @@ -1851,9 +1791,6 @@ playback_start_item(void *arg, int *retval) return COMMAND_END; } - // Update global playback position - pb_pos = TEMP_NEXT_RTPTIME - 88200; - if (player_state == PLAY_STOPPED && !queue_item) { DPRINTF(E_LOG, L_PLAYER, "Failed to start/resume playback, no queue item given\n"); @@ -1864,24 +1801,22 @@ 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); } 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) { @@ -1893,24 +1828,14 @@ playback_start_item(void *arg, int *retval) } } - ret = source_open(ps, TEMP_NEXT_RTPTIME, seek_ms); + ret = playback_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 @@ -2018,70 +1943,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 playback_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, TEMP_NEXT_RTPTIME, 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 = playback_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 @@ -2089,90 +1982,93 @@ playback_prev_bh(void *arg, int *retval) *retval = 0; return COMMAND_END; + + error: + playback_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 playback_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, TEMP_NEXT_RTPTIME, 0); + ret = playback_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: + playback_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 playback_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 = playback_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 @@ -2180,39 +2076,57 @@ playback_seek_bh(void *arg, int *retval) *retval = 0; return COMMAND_END; + + error: + playback_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 playback_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 = playback_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: + playback_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; @@ -2225,29 +2139,10 @@ 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; - } + playback_timer_stop(); *retval = outputs_flush(device_command_cb); - pb_session_stop(); - - source_pause(pos); - outputs_metadata_purge(); // We're async if we need to flush devices @@ -2568,7 +2463,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; } @@ -2624,7 +2519,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); } } @@ -2724,7 +2619,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; @@ -2735,8 +2629,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 { @@ -2794,50 +2690,6 @@ playerqueue_plid(void *arg, int *retval) /* ------------------------------- Player API ------------------------------- */ -// TODO no longer part of API -static 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, &player_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) { @@ -2857,11 +2709,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; } @@ -3418,8 +3270,6 @@ player_deinit(void) player_exit = 1; commands_base_destroy(cmdbase); - session_clear(&pb_session); - ret = pthread_join(tid_player, NULL); if (ret != 0) { diff --git a/src/player.h b/src/player.h index 525d2f03..70bb19bc 100644 --- a/src/player.h +++ b/src/player.h @@ -76,7 +76,7 @@ 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); diff --git a/src/spotify.c b/src/spotify.c index 22f5e255..ffe94bff 100644 --- a/src/spotify.c +++ b/src/spotify.c @@ -718,7 +718,6 @@ 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, NULL, INPUT_FLAG_EOF); *retval = 0; @@ -1042,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, &quality, INPUT_FLAG_NONBLOCK); + input_write(spotify_audio_buffer, &quality, 0); return num_frames; } From d008e241cf4c748a1c6ff57160243fe80bfb5a62 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 17 Feb 2019 00:19:13 +0100 Subject: [PATCH 24/86] [outputs] Make outputs_device_flush (per device flushing) Works better with the callback mechanism --- src/outputs.c | 70 +++++++++++------------ src/outputs.h | 15 +++-- src/outputs/alsa.c | 42 +++++++------- src/outputs/dummy.c | 14 +++++ src/outputs/fifo.c | 34 ++++++------ src/outputs/pulse.c | 76 ++++++++++++------------- src/outputs/raop.c | 132 +++++++++++++++----------------------------- 7 files changed, 171 insertions(+), 212 deletions(-) diff --git a/src/outputs.c b/src/outputs.c index 0fe27e24..adbf33e0 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -124,22 +124,6 @@ callback_remove(struct output_device *device) } } -static void -callback_remove_all(enum output_types type) -{ - struct output_device *device; - - for (device = output_device_list; device; device = device->next) - { - if (type != device->type) - continue; - - outputs_device_cb_set(device, NULL); - - callback_remove(device); - } -} - static int callback_add(struct output_device *device, output_status_cb cb) { @@ -168,7 +152,7 @@ callback_add(struct output_device *device, output_status_cb cb) outputs_cb_queue[callback_id].cb = cb; outputs_cb_queue[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\n", player_pmap(cb), callback_id); + 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_queue); i++) @@ -694,6 +678,18 @@ outputs_device_stop(struct output_device *device, output_status_cb cb) return outputs[device->type]->device_stop(device, callback_add(device, cb)); } +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) { @@ -786,6 +782,25 @@ outputs_playback_stop(void) } } +int +outputs_flush(output_status_cb cb) +{ + struct output_device *device; + int count = 0; + int ret; + + for (device = output_device_list; device; device = device->next) + { + ret = outputs_device_flush(device, cb); + if (ret < 0) + continue; + + count++; + } + + return count; +} + void outputs_write(void *buf, size_t bufsize, struct media_quality *quality, int nsamples, struct timespec *pts) { @@ -805,27 +820,6 @@ outputs_write(void *buf, size_t bufsize, struct media_quality *quality, int nsam buffer_drain(&output_buffer); } -int -outputs_flush(output_status_cb cb) -{ - int ret; - int i; - - ret = 0; - for (i = 0; outputs[i]; i++) - { - if (outputs[i]->disabled || !outputs[i]->flush) - continue; - - // Clear callback register for all devices belonging to outputs[i] - callback_remove_all(outputs[i]->type); - - ret += outputs[i]->flush(callback_add(NULL, cb)); - } - - return ret; -} - struct output_metadata * outputs_metadata_prepare(int id) { diff --git a/src/outputs.h b/src/outputs.h index e3802e78..fb72041b 100644 --- a/src/outputs.h +++ b/src/outputs.h @@ -199,6 +199,9 @@ struct output_definition // 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, int callback_id); @@ -223,9 +226,6 @@ struct output_definition // Write stream data to the output devices void (*write)(struct output_buffer *buffer); - // Flush all sessions, the return must be number of sessions pending the flush - int (*flush)(int callback_id); - // Authorize an output with a pin-code (probably coming from the filescanner) void (*authorize)(const char *pin); @@ -278,6 +278,9 @@ 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_flush(struct output_device *device, output_status_cb cb); + int outputs_device_probe(struct output_device *device, output_status_cb cb); @@ -299,12 +302,12 @@ outputs_device_free(struct output_device *device); void outputs_playback_stop(void); -void -outputs_write(void *buf, size_t bufsize, struct media_quality *quality, int nsamples, struct timespec *pts); - int outputs_flush(output_status_cb cb); +void +outputs_write(void *buf, size_t bufsize, struct media_quality *quality, int nsamples, struct timespec *pts); + struct output_metadata * outputs_metadata_prepare(int id); diff --git a/src/outputs/alsa.c b/src/outputs/alsa.c index a27454d5..3f4e7b8f 100644 --- a/src/outputs/alsa.c +++ b/src/outputs/alsa.c @@ -829,6 +829,24 @@ alsa_device_stop(struct output_session *session) alsa_status(as); } +static int +alsa_device_flush(struct output_device *device, int callback_id) +{ + struct alsa_session *as = device->session; + int i; + + // TODO close device? + + snd_pcm_drop(hdl); + prebuf_free(as); + + as->callback_id = callback_id; + as->state = ALSA_STATE_STARTED; + alsa_status(as); + + return 0; +} + static int alsa_device_probe(struct output_device *device, output_status_cb cb) { @@ -951,28 +969,6 @@ alsa_write(uint8_t *buf, uint64_t 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) { @@ -1059,11 +1055,11 @@ 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, .write = alsa_write, - .flush = alsa_flush, .status_cb = alsa_set_status_cb, }; diff --git a/src/outputs/dummy.c b/src/outputs/dummy.c index 8cf40ad2..8783f01b 100644 --- a/src/outputs/dummy.c +++ b/src/outputs/dummy.c @@ -131,6 +131,19 @@ dummy_device_stop(struct output_device *device, int callback_id) return 0; } +static int +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) { @@ -228,6 +241,7 @@ 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, .device_cb_set = dummy_device_cb_set, diff --git a/src/outputs/fifo.c b/src/outputs/fifo.c index 904a1643..f1fa16e4 100644 --- a/src/outputs/fifo.c +++ b/src/outputs/fifo.c @@ -323,6 +323,21 @@ fifo_device_stop(struct output_device *device, int callback_id) return 0; } +static int +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) { @@ -386,23 +401,6 @@ fifo_playback_stop(void) fifo_status(fifo_session); } -static int -fifo_flush(int callback_id) -{ - struct fifo_session *fifo_session = sessions; - - if (!fifo_session) - return 0; - - fifo_empty(fifo_session); - free_buffer(); - - fifo_session->callback_id = callback_id; - fifo_session->state = OUTPUT_STATE_CONNECTED; - fifo_status(fifo_session); - return 1; -} - static void fifo_write(struct output_buffer *obuf) { @@ -530,10 +528,10 @@ 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, .device_cb_set = fifo_device_cb_set, .playback_stop = fifo_playback_stop, .write = fifo_write, - .flush = fifo_flush, }; diff --git a/src/outputs/pulse.c b/src/outputs/pulse.c index 42e9f044..8afd40c0 100644 --- a/src/outputs/pulse.c +++ b/src/outputs/pulse.c @@ -669,6 +669,40 @@ pulse_device_stop(struct output_session *session) stream_close(ps, close_cb); } + +static int +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, output_status_cb cb) { @@ -824,46 +858,6 @@ pulse_playback_stop(void) 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) { @@ -962,13 +956,13 @@ 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_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 0dd347f3..b69a5aa7 100644 --- a/src/outputs/raop.c +++ b/src/outputs/raop.c @@ -342,9 +342,6 @@ static struct raop_service control_6svc; static struct raop_metadata *metadata_head; static struct raop_metadata *metadata_tail; -/* FLUSH timer */ -static struct event *flush_timer; - /* Keep-alive timer - hack for ATV's with tvOS 10 */ static struct event *keep_alive_timer; static struct timeval keep_alive_tv = { 30, 0 }; @@ -1911,16 +1908,6 @@ session_failure(struct raop_session *rs) session_cleanup(rs); } -static void -deferredev_cb(int fd, short what, void *arg) -{ - struct raop_session *rs = arg; - - DPRINTF(E_DBG, L_RAOP, "Cleaning up failed session (deferred) on device '%s'\n", rs->devname); - - session_failure(rs); -} - static void deferred_session_failure(struct raop_session *rs) { @@ -1974,6 +1961,23 @@ session_teardown(struct raop_session *rs, const char *log_caller) 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) { @@ -2010,6 +2014,7 @@ session_make(struct output_device *rd, int family, int callback_id, bool only_pr } 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->state = RAOP_STATE_STOPPED; rs->only_probe = only_probe; @@ -2059,14 +2064,6 @@ session_make(struct output_device *rd, int family, int callback_id, bool only_pr break; } - rs->deferredev = evtimer_new(evbase_player, 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) { @@ -2153,7 +2150,6 @@ session_make(struct output_device *rd, int family, int callback_id, bool only_pr evrtsp_connection_free(rs->ctrl); out_free_event: event_free(rs->deferredev); - out_free_rs: free(rs); return NULL; @@ -2812,17 +2808,6 @@ raop_cb_keep_alive(struct evrtsp_request *req, void *arg) return; } -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 = raop_sessions; rs; rs = rs->next) - session_teardown(rs, "raop_flush_timer_cb"); -} - static void raop_keep_alive_timer_cb(int fd, short what, void *arg) { @@ -4824,6 +4809,29 @@ raop_device_stop(struct output_device *device, int 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; + struct timeval tv; + 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; + + evutil_timerclear(&tv); + tv.tv_sec = 10; + evtimer_add(rs->deferredev, &tv); + + return 0; +} + static void raop_device_cb_set(struct output_device *device, int callback_id) { @@ -4889,51 +4897,13 @@ raop_write(struct output_buffer *obuf) if (rs->state != RAOP_STATE_CONNECTED) continue; - event_del(flush_timer); // In case playback was stopped but then restarted again + event_del(rs->deferredev); // Kills flush timer in case playback was stopped but then restarted again rs->state = RAOP_STATE_STREAMING; // Make a cb? } } -static int -raop_flush(int callback_id) -{ - struct timeval tv; - struct raop_session *rs; - struct raop_session *next; - int pending; - int ret; - - pending = 0; - for (rs = raop_sessions; rs; rs = next) - { - next = rs->next; - - if (rs->state != RAOP_STATE_STREAMING) - continue; - - ret = raop_send_req_flush(rs, raop_cb_flush, "flush"); - if (ret < 0) - { - session_failure(rs); - continue; - } - - rs->callback_id = callback_id; - pending++; - } - - if (pending > 0) - { - evutil_timerclear(&tv); - tv.tv_sec = 10; - evtimer_add(flush_timer, &tv); - } - - return pending; -} - static int raop_init(void) { @@ -5006,14 +4976,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"); @@ -5022,7 +4985,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); @@ -5055,10 +5018,8 @@ raop_init(void) 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); @@ -5083,7 +5044,6 @@ raop_deinit(void) raop_v2_control_stop(); raop_v2_timing_stop(); - event_free(flush_timer); event_free(keep_alive_timer); gcry_cipher_close(raop_aes_ctx); @@ -5102,6 +5062,7 @@ 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, @@ -5109,7 +5070,6 @@ struct output_definition output_raop = .device_volume_to_pct = raop_volume_to_pct, .playback_stop = raop_playback_stop, .write = raop_write, - .flush = raop_flush, .metadata_prepare = raop_metadata_prepare, .metadata_send = raop_metadata_send, .metadata_purge = raop_metadata_purge, From b7add1d0faab269abb770dc5b5cef29622e2aa34 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 17 Feb 2019 10:41:11 +0100 Subject: [PATCH 25/86] [inputs/player] Fixup part 1 --- src/input.c | 23 +++++++++++++++++------ src/inputs/file_http.c | 5 ++++- src/inputs/pipe.c | 4 +++- src/player.c | 41 +++++++++++++++++++++++++++++++---------- 4 files changed, 55 insertions(+), 18 deletions(-) diff --git a/src/input.c b/src/input.c index 2b633fa0..9f81f207 100644 --- a/src/input.c +++ b/src/input.c @@ -236,7 +236,7 @@ stop(void *arg, int *retval) type = input_now_reading.type; - if (inputs[type]->stop) + if (inputs[type]->stop && input_now_reading.open) inputs[type]->stop(&input_now_reading); flush(&flags); @@ -491,10 +491,18 @@ input_write(struct evbuffer *evbuf, struct media_quality *quality, short flags) if (flags) { - if (input_buffer.bytes_written > INPUT_BUFFER_THRESHOLD) - marker_add(input_buffer.bytes_written - INPUT_BUFFER_THRESHOLD, INPUT_FLAG_START_NEXT); - else - marker_add(input_buffer.bytes_written, INPUT_FLAG_START_NEXT); + if (read_end) + { + input_now_reading.open = false; + // 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); + else + // The player's read is close to our write, so open right away + marker_add(input_buffer.bytes_read, INPUT_FLAG_START_NEXT); + } // Note this marker is added at the post-write position, since EOF, error // and metadata belong there. @@ -578,7 +586,10 @@ play(evutil_socket_t fd, short flags, void *arg) // 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) - return; // Error or EOF, so don't come back + { + input_now_reading.open = false; + return; // Error or EOF, so don't come back + } event_add(inputev, &tv); } diff --git a/src/inputs/file_http.c b/src/inputs/file_http.c index 4c435722..3d8a35c3 100644 --- a/src/inputs/file_http.c +++ b/src/inputs/file_http.c @@ -69,9 +69,12 @@ stop(struct input_source *source) struct transcode_ctx *ctx = source->input_ctx; transcode_cleanup(&ctx); - evbuffer_free(source->evbuf); + + if (source->evbuf) + evbuffer_free(source->evbuf); source->input_ctx = NULL; + source->evbuf = NULL; return 0; } diff --git a/src/inputs/pipe.c b/src/inputs/pipe.c index a13dced1..edd14357 100644 --- a/src/inputs/pipe.c +++ b/src/inputs/pipe.c @@ -840,7 +840,8 @@ stop(struct input_source *source) DPRINTF(E_DBG, L_PLAYER, "Stopping pipe\n"); - evbuffer_free(source->evbuf); + if (source->evbuf) + evbuffer_free(source->evbuf); pipe_close(pipe->fd); @@ -858,6 +859,7 @@ stop(struct input_source *source) pipe_free(pipe); source->input_ctx = NULL; + source->evbuf = NULL; return 0; } diff --git a/src/player.c b/src/player.c index a42cc9da..4d3f2e58 100644 --- a/src/player.c +++ b/src/player.c @@ -668,7 +668,10 @@ source_next_create(struct player_source *current) struct db_queue_item *queue_item; if (!current) - return NULL; + { + DPRINTF(E_LOG, L_PLAYER, "Bug! source_next_create called without a current source\n"); + return NULL; + } queue_item = queue_item_next(current->item_id); if (!queue_item) @@ -783,7 +786,7 @@ session_update_read_next(void) { struct player_source *ps; - ps = source_next_create(pb_session.reading_next); + ps = source_next_create(pb_session.reading_now); source_free(&pb_session.reading_next); pb_session.reading_next = ps; } @@ -799,12 +802,17 @@ session_update_read_eof(void) 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; + // We inherit this because the input will only notify on quality changes, not + // if it 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 @@ -927,12 +935,14 @@ event_read_error() event_read_eof(); } -// Kicks of input reading of next source (async), session is not affected by -// this, so there is no session update +// Kicks of input reading of next source (async) static void event_read_start_next() { - DPRINTF(E_DBG, L_PLAYER, "event_start_next()\n"); + DPRINTF(E_DBG, L_PLAYER, "event_read_start_next()\n"); + + // Attaches next item to session as reading_next + session_update_read_next(); source_next(); } @@ -1024,6 +1034,17 @@ source_read(int *nbytes, int *nsamples, struct media_quality *quality, uint8_t * { short flags; + // Nothing to read, stream silence until event_read() stops playback + if (!pb_session.reading_now && pb_session.playing_now) + { + memset(buf, 0, len); + *quality = pb_session.playing_now->quality; + *nbytes = len; + *nsamples = BTOS(*nbytes, quality->bits_per_sample, quality->channels); + event_read(*nsamples); + return 0; + } + *quality = pb_session.reading_now->quality; *nsamples = 0; *nbytes = input_read(buf, len, &flags); @@ -1110,14 +1131,14 @@ playback_cb(int fd, short what, void *arg) pb_write_recovery = false; } -// debug_counter++; -// if (debug_counter % 100 == 0) -// session_dump(); + debug_counter++; + if (debug_counter % 100 == 0) + session_dump(); // 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. - for (i = 1 + overrun + pb_read_deficit; i > 0 && pb_session.reading_now; i--) + for (i = 1 + overrun + pb_read_deficit; i > 0; i--) { ret = source_read(&nbytes, &nsamples, &quality, pb_session.buffer, pb_session.bufsize); if (ret < 0) From c74b3059839036083f77729e36e392aa45e04cbd Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 17 Feb 2019 13:12:29 +0100 Subject: [PATCH 26/86] [outputs] Fix crash, invalid reference to device after outputs_device_add --- src/outputs.c | 4 ++-- src/outputs.h | 5 ++++- src/player.c | 9 +++++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/outputs.c b/src/outputs.c index adbf33e0..fd498b25 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -527,7 +527,7 @@ outputs_listener_notify(void) /* ---------------------------- Called by player ---------------------------- */ -int +struct output_device * outputs_device_add(struct output_device *add, bool new_deselect, int default_volume) { struct output_device *device; @@ -603,7 +603,7 @@ outputs_device_add(struct output_device *add, bool new_deselect, int default_vol listener_notify(LISTENER_SPEAKER); - return 0; + return device; } void diff --git a/src/outputs.h b/src/outputs.h index fb72041b..51bfa82f 100644 --- a/src/outputs.h +++ b/src/outputs.h @@ -266,7 +266,10 @@ outputs_listener_notify(void); /* ---------------------------- Called by player ---------------------------- */ -int +// 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 diff --git a/src/player.c b/src/player.c index 4d3f2e58..7d560c24 100644 --- a/src/player.c +++ b/src/player.c @@ -794,8 +794,8 @@ session_update_read_next(void) static void session_update_read_eof(void) { - pb_session.reading_now->read_end = pb_session.pos - 1; - pb_session.reading_now->play_end = pb_session.pos - 1 + pb_session.reading_now->output_buffer_samples; + 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; @@ -1196,9 +1196,10 @@ device_add(void *arg, int *retval) // Never turn on new devices during playback new_deselect = (player_state == PLAY_PLAYING); - *retval = outputs_device_add(device, new_deselect, default_volume); + device = outputs_device_add(device, new_deselect, default_volume); + *retval = device ? 0 : -1; - if (device->selected) + if (device && device->selected) speaker_select_output(device); return COMMAND_END; From de55acf79700793423664cf670cb5e90f23e57ac Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 17 Feb 2019 13:13:45 +0100 Subject: [PATCH 27/86] [input] Fixup 2: Seeking when rewinding to 0 --- src/input.c | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/input.c b/src/input.c index 9f81f207..dfeebd70 100644 --- a/src/input.c +++ b/src/input.c @@ -226,8 +226,8 @@ flush(short *flags) #endif } -static enum command_state -stop(void *arg, int *retval) +static void +stop(void) { short flags; int type; @@ -242,15 +242,12 @@ stop(void *arg, int *retval) flush(&flags); clear(&input_now_reading); - - *retval = 0; - return COMMAND_END; } static int seek(struct input_source *source, int seek_ms) { - if (seek_ms > 0 && inputs[source->type]->seek) + if (inputs[source->type]->seek) return inputs[source->type]->seek(source, seek_ms); else return 0; @@ -287,14 +284,17 @@ setup(struct input_source *source, struct db_queue_item *queue_item, int seek_ms source->open = true; - ret = seek(source, seek_ms); - if (ret < 0) - goto seek_error; + if (seek_ms > 0) + { + ret = seek(source, seek_ms); + if (ret < 0) + goto seek_error; + } return ret; seek_error: - stop(NULL, NULL); + stop(); setup_error: clear(source); return -1; @@ -320,7 +320,7 @@ start(void *arg, int *retval) else { if (input_now_reading.open) - stop(NULL, NULL); + stop(); // Get the queue_item from the db queue_item = db_queue_fetch_byitemid(cmdarg->item_id); @@ -351,6 +351,15 @@ start(void *arg, int *retval) return COMMAND_END; } +static enum command_state +stop_cmd(void *arg, int *retval) +{ + stop(); + + *retval = 0; + return COMMAND_END; +} + /* static enum command_state next(void *arg, int *retval) @@ -699,7 +708,7 @@ input_start(uint32_t item_id) void input_stop(void) { - commands_exec_async(cmdbase, stop, NULL); + commands_exec_async(cmdbase, stop_cmd, NULL); } void From 201c97fdf8c36a34c765a24f375e9610bfcc3459 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 17 Feb 2019 16:41:37 +0100 Subject: [PATCH 28/86] Fixup 3 - remove small risk of crash if encode_setup called incorrectly --- src/transcode.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/transcode.c b/src/transcode.c index 74146f70..11831822 100644 --- a/src/transcode.c +++ b/src/transcode.c @@ -340,7 +340,8 @@ make_wav_header(struct encode_ctx *ctx, struct decode_ctx *src_ctx, off_t *est_s bps = av_get_bits_per_sample(ctx->settings.audio_codec); wav_len = ctx->settings.channels * (bps / 8) * 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); From 9dfab161388310489993671a489cab07ca995527 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 17 Feb 2019 21:48:48 +0100 Subject: [PATCH 29/86] [streaming] Cleaning bugs from refactor --- src/httpd_streaming.c | 46 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/src/httpd_streaming.c b/src/httpd_streaming.c index c7d50e61..c5db2577 100644 --- a/src/httpd_streaming.c +++ b/src/httpd_streaming.c @@ -112,9 +112,31 @@ 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) { @@ -122,12 +144,12 @@ streaming_meta_cb(evutil_socket_t fd, short event, void *arg) 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; - streaming_quality = quality; - decode_ctx = NULL; if (quality.sample_rate == 44100 && quality.bits_per_sample == 16) decode_ctx = transcode_decode_setup_raw(XCODE_PCM16_44100); @@ -150,12 +172,15 @@ streaming_meta_cb(evutil_socket_t fd, short event, void *arg) return; } + streaming_quality = quality; streaming_not_supported = 0; + return; + error: - DPRINTF(E_LOG, L_STREAMING, "Unknown or unsupported quality of input data, cannot MP3 encode\n"); - transcode_encode_cleanup(&streaming_encode_ctx); + 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 @@ -165,6 +190,12 @@ encode_buffer(uint8_t *buffer, size_t size) 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.sample_rate, streaming_quality.bits_per_sample); @@ -290,7 +321,7 @@ streaming_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parse char *address; ev_uint16_t port; - if (!streaming_not_supported) + if (streaming_not_supported) { DPRINTF(E_LOG, L_STREAMING, "Got MP3 streaming request, but cannot encode to MP3\n"); @@ -329,7 +360,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; From 19694ee1dd9cd6b259f3ba0f6fdedeec50ecdead Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 22 Feb 2019 08:36:27 +0100 Subject: [PATCH 30/86] [misc] Add ringbuffer utility to misc.c/misc.h --- src/misc.c | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/misc.h | 21 ++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/src/misc.c b/src/misc.c index e65ce35a..423d95c9 100644 --- a/src/misc.c +++ b/src/misc.c @@ -1082,6 +1082,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 dcd0396c..8d95b17c 100644 --- a/src/misc.h +++ b/src/misc.h @@ -46,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); @@ -137,6 +146,18 @@ quality_is_equal(struct media_quality *a, struct media_quality *b); 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 From 5c35e1a9db33cde68cb76620f313da9ef84da402 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 22 Feb 2019 08:37:50 +0100 Subject: [PATCH 31/86] [player] Completely stop devices on playback_stop() Previously we just flushed, and local audio devices would never get closed. Also a little fixup to the printing of session debug. --- src/player.c | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/player.c b/src/player.c index 7d560c24..f9496497 100644 --- a/src/player.c +++ b/src/player.c @@ -126,6 +126,8 @@ // (value is in milliseconds) #define PLAYER_WRITE_BEHIND_MAX 1500 +//#define DEBUG_PLAYER 1 + struct volume_param { int volume; uint64_t spk_id; @@ -243,9 +245,6 @@ struct player_session struct player_source *playing_now; }; - -static int debug_counter = -1; - static struct player_session pb_session; struct event_base *evbase_player; @@ -716,6 +715,9 @@ source_start(void) // 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; + static int source_print(char *line, size_t linesize, struct player_source *ps, const char *name) { @@ -740,11 +742,18 @@ source_print(char *line, size_t linesize, struct player_source *ps, const char * } static void -session_dump(void) +session_dump(bool use_counter) { char line[4096]; int pos = 0; + if (use_counter) + { + debug_dump_counter++; + if (debug_dump_counter % 100 != 0) + return; + } + 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"); @@ -768,6 +777,7 @@ session_dump(void) DPRINTF(E_DBG, L_PLAYER, "%s\n", line); } +#endif static void session_update_play_eof(void) @@ -1131,9 +1141,9 @@ playback_cb(int fd, short what, void *arg) pb_write_recovery = false; } - debug_counter++; - if (debug_counter % 100 == 0) - session_dump(); +#ifdef DEBUG_PLAYER + session_dump(true); +#endif // 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 @@ -1754,20 +1764,10 @@ playback_stop(void *arg, int *retval) 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); - - playback_session_stop(); + playback_abort(); status_update(PLAY_STOPPED); - outputs_metadata_purge(); - - // We're async if we need to flush devices - if (*retval > 0) - return COMMAND_PENDING; - return COMMAND_END; } From b8111225663ed46451e63f3d8e22218e07c25825 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 22 Feb 2019 08:40:59 +0100 Subject: [PATCH 32/86] [outputs] Rename output_buffer.frame to .data --- src/httpd_streaming.c | 6 +++--- src/outputs.c | 40 ++++++++++++++++++++-------------------- src/outputs.h | 4 ++-- src/outputs/fifo.c | 12 ++++++------ src/outputs/raop.c | 8 ++++---- 5 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/httpd_streaming.c b/src/httpd_streaming.c index c5db2577..c26b6dde 100644 --- a/src/httpd_streaming.c +++ b/src/httpd_streaming.c @@ -290,9 +290,9 @@ streaming_write(struct output_buffer *obuf) if (!streaming_sessions) return; - if (!quality_is_equal(&obuf->frames[0].quality, &streaming_quality)) + if (!quality_is_equal(&obuf->data[0].quality, &streaming_quality)) { - ret = write(streaming_meta[1], &obuf->frames[0].quality, sizeof(struct media_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)); @@ -300,7 +300,7 @@ streaming_write(struct output_buffer *obuf) } } - ret = write(streaming_pipe[1], obuf->frames[0].buffer, obuf->frames[0].bufsize); + ret = write(streaming_pipe[1], obuf->data[0].buffer, obuf->data[0].bufsize); if (ret < 0) { if (errno == EAGAIN) diff --git a/src/outputs.c b/src/outputs.c index fd498b25..6bde6c51 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -290,20 +290,20 @@ buffer_fill(struct output_buffer *obuf, void *buf, size_t bufsize, struct media_ // 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->frames[0].quality) || output_got_new_subscription) + if (!quality_is_equal(quality, &obuf->data[0].quality) || output_got_new_subscription) { encoding_reset(quality); output_got_new_subscription = false; } - // The first element of the output_buffer is always just the raw input frame + // 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->frames[0].evbuf, buf, bufsize); - obuf->frames[0].buffer = buf; - obuf->frames[0].bufsize = bufsize; - obuf->frames[0].quality = *quality; - obuf->frames[0].samples = nsamples; + 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++) { @@ -314,15 +314,15 @@ buffer_fill(struct output_buffer *obuf, void *buf, size_t bufsize, struct media_ if (!frame) continue; - ret = transcode_encode(obuf->frames[n].evbuf, output_quality_subscriptions[i].encode_ctx, frame, 0); + ret = transcode_encode(obuf->data[n].evbuf, output_quality_subscriptions[i].encode_ctx, frame, 0); transcode_frame_free(frame); if (ret < 0) continue; - obuf->frames[n].buffer = evbuffer_pullup(obuf->frames[n].evbuf, -1); - obuf->frames[n].bufsize = evbuffer_get_length(obuf->frames[n].evbuf); - obuf->frames[n].quality = output_quality_subscriptions[i].quality; - obuf->frames[n].samples = BTOS(obuf->frames[n].bufsize, obuf->frames[n].quality.bits_per_sample, obuf->frames[n].quality.channels); + 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++; } } @@ -332,11 +332,11 @@ buffer_drain(struct output_buffer *obuf) { int i; - for (i = 0; obuf->frames[i].buffer; i++) + for (i = 0; obuf->data[i].buffer; i++) { - evbuffer_drain(obuf->frames[i].evbuf, obuf->frames[i].bufsize); - obuf->frames[i].buffer = NULL; - obuf->frames[i].bufsize = 0; + 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 } } @@ -982,8 +982,8 @@ outputs_init(void) if (no_output) return -1; - for (i = 0; i < ARRAY_SIZE(output_buffer.frames); i++) - output_buffer.frames[i].evbuf = evbuffer_new(); + for (i = 0; i < ARRAY_SIZE(output_buffer.data); i++) + output_buffer.data[i].evbuf = evbuffer_new(); return 0; } @@ -1012,7 +1012,7 @@ outputs_deinit(void) memset(&output_quality_subscriptions[i], 0, sizeof(struct output_quality_subscription)); } - for (i = 0; i < ARRAY_SIZE(output_buffer.frames); i++) - evbuffer_free(output_buffer.frames[i].evbuf); + 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 51bfa82f..49e62892 100644 --- a/src/outputs.h +++ b/src/outputs.h @@ -153,7 +153,7 @@ struct output_metadata struct output_metadata *next; }; -struct output_frame +struct output_data { struct media_quality quality; struct evbuffer *evbuf; @@ -166,7 +166,7 @@ struct output_buffer { uint32_t write_counter; // REMOVE ME? not used for anything struct timespec pts; - struct output_frame frames[OUTPUTS_MAX_QUALITY_SUBSCRIPTIONS + 1]; + struct output_data data[OUTPUTS_MAX_QUALITY_SUBSCRIPTIONS + 1]; } output_buffer; typedef void (*output_status_cb)(struct output_device *device, enum output_device_state status); diff --git a/src/outputs/fifo.c b/src/outputs/fifo.c index f1fa16e4..f1f54e80 100644 --- a/src/outputs/fifo.c +++ b/src/outputs/fifo.c @@ -413,13 +413,13 @@ fifo_write(struct output_buffer *obuf) if (!fifo_session) return; - for (i = 0; obuf->frames[i].buffer; i++) + for (i = 0; obuf->data[i].buffer; i++) { - if (quality_is_equal(&fifo_quality, &obuf->frames[i].quality)) + if (quality_is_equal(&fifo_quality, &obuf->data[i].quality)) break; } - if (!obuf->frames[i].buffer) + if (!obuf->data[i].buffer) { DPRINTF(E_LOG, L_FIFO, "Bug! Did not get audio in quality required\n"); return; @@ -428,10 +428,10 @@ fifo_write(struct output_buffer *obuf) fifo_session->state = OUTPUT_STATE_STREAMING; CHECK_NULL(L_FIFO, packet = calloc(1, sizeof(struct fifo_packet))); - CHECK_NULL(L_FIFO, packet->samples = malloc(obuf->frames[i].bufsize)); + CHECK_NULL(L_FIFO, packet->samples = malloc(obuf->data[i].bufsize)); - memcpy(packet->samples, obuf->frames[i].buffer, obuf->frames[i].bufsize); - packet->samples_size = obuf->frames[i].bufsize; + memcpy(packet->samples, obuf->data[i].buffer, obuf->data[i].bufsize); + packet->samples_size = obuf->data[i].bufsize; packet->pts = obuf->pts; if (buffer.head) diff --git a/src/outputs/raop.c b/src/outputs/raop.c index b69a5aa7..860fac8d 100644 --- a/src/outputs/raop.c +++ b/src/outputs/raop.c @@ -4868,16 +4868,16 @@ raop_write(struct output_buffer *obuf) for (rms = raop_master_sessions; rms; rms = rms->next) { - for (i = 0; obuf->frames[i].buffer; i++) + for (i = 0; obuf->data[i].buffer; i++) { - if (!quality_is_equal(&obuf->frames[i].quality, &rms->rtp_session->quality)) + 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); - evbuffer_add_buffer_reference(rms->evbuf, obuf->frames[i].evbuf); - rms->evbuf_samples += obuf->frames[i].samples; + evbuffer_add_buffer_reference(rms->evbuf, obuf->data[i].evbuf); + 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) From 9d5555152c53ff0e5e1d89bae5d8240e731cb0c3 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 22 Feb 2019 08:41:33 +0100 Subject: [PATCH 33/86] [alsa] Implement new output interface + some refactoring Sync still missing --- src/outputs/alsa.c | 1577 ++++++++++++++++++++++---------------------- 1 file changed, 793 insertions(+), 784 deletions(-) diff --git a/src/outputs/alsa.c b/src/outputs/alsa.c index 3f4e7b8f..7ac76c3a 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" @@ -50,28 +49,8 @@ // within the 10 seconds where we measure latency each second. #define ALSA_MAX_LATENCY_VARIANCE 352 -// 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; - #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, @@ -81,69 +60,694 @@ 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; + snd_pcm_status_t *pcm_status; + + struct media_quality quality; + + int buffer_nsamp; + + uint32_t pos; + uint32_t start_pos; + + struct timespec start_pts; + struct timespec last_pts; + snd_htimestamp_t dev_start_ts; + + snd_pcm_sframes_t last_avail; 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 - // 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; + // Here we buffer samples during startup + struct ringbuffer prebuf; + + int offset; + int adjust_period_seconds; int volume; + long vol_min; + long vol_max; - struct event *deferredev; - output_status_cb defer_cb; - - struct output_device *device; - 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 */ +// 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) + { + DPRINTF(E_LOG, L_LAUDIO, "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; + } + + 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 inline void +device_timestamp(struct alsa_session *as, snd_pcm_sframes_t *delay, snd_pcm_sframes_t *avail, snd_htimestamp_t *ts) +{ + snd_pcm_status(as->hdl, as->pcm_status); + + if (delay) + *delay = snd_pcm_status_get_delay(as->pcm_status); + if (avail) + *avail = snd_pcm_status_get_avail(as->pcm_status); + + snd_pcm_status_get_htstamp(as->pcm_status, ts); +} + +static void +playback_restart(struct alsa_session *as, struct output_buffer *obuf) +{ + snd_pcm_state_t state; + snd_pcm_sframes_t delay; + 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->start_pos = 0; + as->pos = 0; + + // Time stamps used for syncing + as->start_pts = obuf->pts; + + device_timestamp(as, &delay, &as->last_avail, &as->dev_start_ts); + if (as->dev_start_ts.tv_sec == 0 && as->dev_start_ts.tv_nsec == 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Can't get timestamps from ALSA, sync check is disabled\n"); + } + + // The difference between pos and start_pos should match the 2 second buffer + // that AirPlay uses (OUTPUTS_BUFFER_DURATION). 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. Delay might be + // non-zero if we are restarting (?). + as->buffer_nsamp = OUTPUTS_BUFFER_DURATION * as->quality.sample_rate - delay; + 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(snd_pcm_sframes_t *delay, snd_pcm_sframes_t *avail, struct alsa_session *as, struct timespec pts) +{ + enum alsa_sync_state sync; + snd_htimestamp_t ts; + uint64_t elapsed; + uint64_t dev_elapsed; + uint64_t pos; + uint64_t dev_pos; + uint32_t buffered_samples; + int32_t latency; + + // We don't need avail for the sync check, but to reduce querying we retrieve + // it here as a service for the caller + device_timestamp(as, delay, avail, &ts); + if (ts.tv_sec == 0 && ts.tv_nsec == 0) + return ALSA_SYNC_OK; + + // Here we calculate elapsed time since we started, or since we last reset the + // sync timers: elapsed is how long the player thinks has elapsed, dev_elapsed + // is how long ALSA thinks has elapsed. If these are different, but the + // playback positition is the same, then the ALSA clock has drifted and we are + // coming out of sync. Unit is milliseconds. + elapsed = (pts.tv_sec - as->start_pts.tv_sec) * 1000L + (pts.tv_nsec - as->start_pts.tv_nsec) / 1000000; + dev_elapsed = (ts.tv_sec - as->dev_start_ts.tv_sec) * 1000L + (ts.tv_nsec - as->dev_start_ts.tv_nsec) / 1000000; + + // Now calculate playback positions. The pos is where we should be, dev_pos is + // where we actually are. + pos = as->start_pos + (elapsed - 1000 * OUTPUTS_BUFFER_DURATION) * as->quality.sample_rate / 1000; + buffered_samples = *delay + BTOS(as->prebuf.read_avail, as->quality.bits_per_sample, as->quality.channels); + dev_pos = as->start_pos + dev_elapsed * as->quality.sample_rate / 1000 - buffered_samples; + + // TODO calculate below and above more efficiently? + latency = pos - dev_pos; + + // If the latency is low or very different from our last measurement, we will wait and see + if (abs(latency) < ALSA_MAX_LATENCY || abs(as->last_latency - latency) > ALSA_MAX_LATENCY_VARIANCE) + sync = ALSA_SYNC_OK; + else if (latency > 0) + sync = ALSA_SYNC_BEHIND; + else + sync = ALSA_SYNC_AHEAD; + + // The will be used by sync_correct, so it knows how much we are out of sync + as->last_latency = latency; + + DPRINTF(E_DBG, L_LAUDIO, "Sync=%d, pos=%lu, as->pos=%u, dev_pos=%lu, latency=%d, delay=%li, avail=%li, elapsed=%lu, dev_elapsed=%lu\n", + sync, pos, as->pos, dev_pos, latency, *delay, *avail, elapsed / 1000000, dev_elapsed / 1000000); + + return sync; +} + +static void +sync_correct(void) +{ + // Not implemented yet +} + +static void +playback_write(struct alsa_session *as, struct output_buffer *obuf) +{ + snd_pcm_sframes_t ret; + snd_pcm_sframes_t delay; + enum alsa_sync_state sync; + 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 time a second has passed + if (obuf->pts.tv_sec != as->last_pts.tv_sec) + { + sync = sync_check(&delay, &as->last_avail, as, obuf->pts); + if (sync != ALSA_SYNC_OK) + sync_correct(); + + as->last_pts = obuf->pts; + } + + ret = buffer_write(as, &obuf->data[i], as->last_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); } @@ -166,883 +770,289 @@ alsa_session_cleanup(struct alsa_session *as) s->next = as->next; } - as->device->session = NULL; + 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 original_adjust; + int ret; CHECK_NULL(L_LAUDIO, as = calloc(1, sizeof(struct alsa_session))); - as->deferredev = evtimer_new(evbase_player, defer_cb, as); - if (!as->deferredev) + as->device_id = device->id; + as->callback_id = callback_id; + as->volume = device->volume; + + 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"); + + // TODO implement + as->offset = cfg_getint(cfg_audio, "offset"); + if (abs(as->offset) > 44100) { - DPRINTF(E_LOG, L_LAUDIO, "Out of memory for ALSA deferred event\n"); - goto failure_cleanup; + DPRINTF(E_LOG, L_LAUDIO, "The ALSA offset (%d) set in the configuration is out of bounds\n", as->offset); + as->offset = 44100 * (as->offset/abs(as->offset)); } - as->state = ALSA_STATE_STOPPED; - as->device = device; - as->status_cb = cb; - 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 + // TODO implement + original_adjust = cfg_getint(cfg_audio, "adjust_period_seconds"); + if (original_adjust < 1) + as->adjust_period_seconds = 1; + else if (original_adjust > 20) + as->adjust_period_seconds = 20; + else + as->adjust_period_seconds = original_adjust; + if (as->adjust_period_seconds != original_adjust) + DPRINTF(E_LOG, L_LAUDIO, "Clamped ALSA adjust_period_seconds to %d\n", as->adjust_period_seconds); + 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; - outputs_device_session_add(device, 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) / ALSA_SAMPLES_PER_PACKET + 1; - if (as->prebuf_len > (3 * 44100 - offset) / ALSA_SAMPLES_PER_PACKET) - { - 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 * ALSA_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 - ALSA_SAMPLES_PER_PACKET; - - // 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 = ALSA_SAMPLES_PER_PACKET; - - if (as->prebuf && (prebuffering || !prebuf_empty || *avail < ALSA_SAMPLES_PER_PACKET)) - { - pkt = &as->prebuf[as->prebuf_head * ALSA_PACKET_SIZE]; - - memcpy(pkt, buf, ALSA_PACKET_SIZE); - - as->prebuf_head = (as->prebuf_head + 1) % as->prebuf_len; - - if (prebuffering || *avail < ALSA_SAMPLES_PER_PACKET) - 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 * ALSA_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 * ALSA_SAMPLES_PER_PACKET; - while (nsamp > *avail) - { - npackets -= 1; - nsamp -= ALSA_SAMPLES_PER_PACKET; - } - - 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 - ALSA_SAMPLES_PER_PACKET * 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 += ALSA_SAMPLES_PER_PACKET; - - 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 + ALSA_SAMPLES_PER_PACKET * (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 -= ALSA_SAMPLES_PER_PACKET; -} - /* ------------------ 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; + as->state = OUTPUT_STATE_CONNECTED; alsa_status(as); return 0; } -static void -alsa_device_stop(struct output_session *session) +static int +alsa_device_stop(struct output_device *device, int callback_id) { - struct alsa_session *as = session->session; + struct alsa_session *as = device->session; - device_close(); + as->callback_id = callback_id; + as->state = OUTPUT_STATE_STOPPED; + alsa_status(as); // Will terminate the session since the state is STOPPED - as->state = ALSA_STATE_STOPPED; - alsa_status(as); + return 0; } static int alsa_device_flush(struct output_device *device, int callback_id) { struct alsa_session *as = device->session; - int i; - // TODO close device? + snd_pcm_drop(as->hdl); - snd_pcm_drop(hdl); - prebuf_free(as); + ringbuffer_free(&as->prebuf, 1); as->callback_id = callback_id; - as->state = ALSA_STATE_STARTED; + as->state = OUTPUT_STATE_CONNECTED; alsa_status(as); return 0; } static int -alsa_device_probe(struct output_device *device, output_status_cb cb) +alsa_device_probe(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; - } - - device_close(); - - as->state = ALSA_STATE_STOPPED; - alsa_status(as); + 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, output_status_cb cb) +alsa_device_volume_set(struct output_device *device, int callback_id) { - struct alsa_session *as; + 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) { 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; + snd_pcm_drop(as->hdl); - as->state = ALSA_STATE_STARTED; - alsa_status(as); + as->state = OUTPUT_STATE_STOPPED; + alsa_status(as); // Will stop the session } } static void -alsa_write(uint8_t *buf, uint64_t rtptime) +alsa_write(struct output_buffer *obuf) { struct alsa_session *as; - uint64_t pos; + struct alsa_session *next; - for (as = sessions; as; as = as->next) + for (as = sessions; as; as = next) { - if (as->state == ALSA_STATE_STARTED) - { - playback_pos_get(&pos, rtptime); + 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); - DPRINTF(E_DBG, L_LAUDIO, "Starting ALSA device '%s' (pos %" PRIu64 ", rtptime %" PRIu64 ")\n", as->devname, pos, rtptime); + playback_write(as, obuf); - playback_start(as, pos, rtptime); - } - - playback_write(as, buf, rtptime); + alsa_last_quality = obuf->data[0].quality; } } -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)); - } - - 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->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 = @@ -1058,8 +1068,7 @@ struct output_definition output_alsa = .device_flush = alsa_device_flush, .device_probe = alsa_device_probe, .device_volume_set = alsa_device_volume_set, - .playback_start = alsa_playback_start, + .device_cb_set = alsa_device_cb_set, .playback_stop = alsa_playback_stop, .write = alsa_write, - .status_cb = alsa_set_status_cb, }; From cfaceab9fe7e3eeb5a1f6f21d5c3149cab16fad6 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 22 Feb 2019 09:12:38 +0100 Subject: [PATCH 34/86] [outputs] Misc fixup --- src/outputs/alsa.c | 2 +- src/outputs/raop.c | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/outputs/alsa.c b/src/outputs/alsa.c index 7ac76c3a..c77e48fb 100644 --- a/src/outputs/alsa.c +++ b/src/outputs/alsa.c @@ -648,7 +648,7 @@ sync_check(snd_pcm_sframes_t *delay, snd_pcm_sframes_t *avail, struct alsa_sessi // The will be used by sync_correct, so it knows how much we are out of sync as->last_latency = latency; - DPRINTF(E_DBG, L_LAUDIO, "Sync=%d, pos=%lu, as->pos=%u, dev_pos=%lu, latency=%d, delay=%li, avail=%li, elapsed=%lu, dev_elapsed=%lu\n", + DPRINTF(E_DBG, L_LAUDIO, "Sync=%d, pos=%" PRIu64 ", as->pos=%u, dev_pos=%" PRIu64 ", latency=%d, delay=%li, avail=%li, elapsed=%" PRIu64 ", dev_elapsed=%" PRIu64 "\n", sync, pos, as->pos, dev_pos, latency, *delay, *avail, elapsed / 1000000, dev_elapsed / 1000000); return sync; diff --git a/src/outputs/raop.c b/src/outputs/raop.c index 860fac8d..7528e040 100644 --- a/src/outputs/raop.c +++ b/src/outputs/raop.c @@ -213,7 +213,7 @@ struct raop_session int family; int volume; - uint64_t start_rtptime; + uint32_t start_rtptime; /* AirTunes v2 */ unsigned short server_port; @@ -244,8 +244,8 @@ struct raop_metadata int artwork_fmt; /* Progress data */ - uint64_t start; - uint64_t end; + uint32_t start; + uint32_t end; struct raop_metadata *next; }; @@ -2327,7 +2327,7 @@ raop_cb_metadata(struct evrtsp_request *req, void *arg) } static int -raop_metadata_send_progress(struct raop_session *rs, struct evbuffer *evbuf, struct raop_metadata *rmd, uint64_t offset, uint32_t delay) +raop_metadata_send_progress(struct raop_session *rs, struct evbuffer *evbuf, struct raop_metadata *rmd, uint32_t offset, uint32_t delay) { uint32_t display; int ret; @@ -2354,7 +2354,7 @@ raop_metadata_send_progress(struct raop_session *rs, struct evbuffer *evbuf, str return -1; } - DPRINTF(E_DBG, L_PLAYER, "Metadata send is start_time=%zu, start=%zu, display=%zu, current=%zu, end=%zu\n", + DPRINTF(E_DBG, L_PLAYER, "Metadata send is start_time=%" PRIu32 ", start=%" PRIu32 ", display=%" PRIu32 ", current=%" PRIu32 ", end=%" PRIu32 "\n", rs->start_rtptime, rmd->start, rmd->start - delay, rmd->start + offset, rmd->end); ret = raop_send_req_set_parameter(rs, evbuf, "text/parameters", NULL, raop_cb_metadata, "send_progress"); @@ -2432,7 +2432,7 @@ 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_internal(struct raop_session *rs, struct raop_metadata *rmd, uint32_t offset, uint32_t delay) { char rtptime[32]; struct evbuffer *evbuf; @@ -2496,7 +2496,7 @@ static void raop_metadata_startup_send(struct raop_session *rs) { struct raop_metadata *rmd; - uint64_t offset; + uint32_t offset; int sent; int ret; @@ -4876,7 +4876,8 @@ raop_write(struct output_buffer *obuf) // Sends sync packets to new sessions, and if it is sync time then also to old sessions packets_sync_send(rms, obuf->pts); - evbuffer_add_buffer_reference(rms->evbuf, obuf->data[i].evbuf); + // 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) From 107fa3634b22db73b677672c11f8c804b30c0823 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 22 Feb 2019 15:53:13 +0100 Subject: [PATCH 35/86] [spotify] Remove obsolete .start reference --- src/inputs/spotify.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/inputs/spotify.c b/src/inputs/spotify.c index cddc1424..60cd3ecc 100644 --- a/src/inputs/spotify.c +++ b/src/inputs/spotify.c @@ -86,7 +86,6 @@ struct input_definition input_spotify = .type = INPUT_TYPE_SPOTIFY, .disabled = 0, .setup = setup, - .start = start, .stop = stop, .seek = seek, }; From b56b56af07bbea77968c2565b30c9f3b585b0541 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 22 Feb 2019 15:53:59 +0100 Subject: [PATCH 36/86] [input] Cleaning up --- src/input.c | 65 +++-------------------------------------------------- 1 file changed, 3 insertions(+), 62 deletions(-) diff --git a/src/input.c b/src/input.c index dfeebd70..ca6a3fcd 100644 --- a/src/input.c +++ b/src/input.c @@ -41,9 +41,9 @@ #include "commands.h" #include "input.h" -// Disallow further writes to the buffer when its size is larger than this threshold -// TODO untie from 44100 -#define INPUT_BUFFER_THRESHOLD STOB(88200, 16, 2) +// 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 in playback thread #define INPUT_LOOP_TIMEOUT 1 @@ -360,65 +360,6 @@ stop_cmd(void *arg, int *retval) return COMMAND_END; } -/* -static enum command_state -next(void *arg, int *retval) -{ - struct player_status status; - struct db_queue_item *queue_item; - uint32_t item_id; - int type; - int ret; - - // We may have finished reading source way before end of playback, and we - // don't want to proceed prematurely. So we wait until the input buffer is - // below the write threshold. - ret = input_wait(); - if (ret < 0) - { - input_next(); // Async call to ourselves - return; - } - - item_id = input_now_reading.item_id; - - // Cleans up the source that has ended/failed and clears input_now_reading - stop(NULL, NULL); - - player_get_status(&status); - - // TODO what about repeat/repeat_all? Maybe move next() to player that can - // just call input_start() - - // Get the next queue_item from the db - queue_item = db_queue_fetch_next(item_id, status.shuffle); - if (!queue_item) - { - DPRINTF(E_DBG, L_PLAYER, "Reached end of playback queue\n"); - *retval = 0; - return COMMAND_END; - } - - ret = setup(&input_now_reading, queue_item, 0); - free_queue_item(queue_item, 0); - if (ret < 0) - goto error; - - DPRINTF(E_DBG, L_PLAYER, "Continuing input read loop for item '%s' (item id %" PRIu32 ")\n", input_now_reading.path, input_now_reading.item_id); - - event_active(inputev, 0, 0); - - *retval = 0; - return COMMAND_END; - - error: - input_write(NULL, NULL, INPUT_FLAG_ERROR); - clear(&input_now_reading); - *retval = -1; - return COMMAND_END; -} -*/ - static enum command_state metadata_get(void *arg, int *retval) { From 2472aa9463913c15fe24bbb5426aaa2a862c9cbe Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 22 Feb 2019 15:54:25 +0100 Subject: [PATCH 37/86] [player] Add timer to stop playback after being paused for a while --- src/player.c | 99 +++++++++++++++++++++++++--------------------------- 1 file changed, 47 insertions(+), 52 deletions(-) diff --git a/src/player.c b/src/player.c index f9496497..6da93b61 100644 --- a/src/player.c +++ b/src/player.c @@ -126,6 +126,12 @@ // (value is in milliseconds) #define PLAYER_WRITE_BEHIND_MAX 1500 +// When we pause, we keep the input open, but we can't do that forever. We must +// think of the poor streaming servers, for instance. This timeout determines +// how long we stay paused, before we go to a full stop. +// (value is in seconds) +#define PLAYER_PAUSE_TIME_MAX 600 + //#define DEBUG_PLAYER 1 struct volume_param { @@ -226,10 +232,10 @@ struct player_session // The time the playback session started struct timespec start_ts; - // The time the first sample in the buffer should be played by the output. + // 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 + OUTPUTS_BUFFER_DURATION + ticks_elapsed * player_tick_interval -// TODO is above still correct? + // pts = start_ts + ticks_elapsed * player_tick_interval struct timespec pts; // Equals current number of samples written to outputs @@ -274,12 +280,15 @@ static int pb_timer_fd; timer_t pb_timer; #endif static struct event *pb_timer_ev; +static struct event *player_pause_timeout_ev; // Time between ticks, i.e. time between when playback_cb() is invoked static struct timespec player_tick_interval; // Timer resolution static struct timespec player_timer_res; +static struct timeval player_pause_timeout = { PLAYER_PAUSE_TIME_MAX, 0 }; + // How many writes we owe the output (when the input is underrunning) static int pb_read_deficit; @@ -290,13 +299,6 @@ 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 status static int output_sessions; @@ -307,10 +309,6 @@ static int master_volume; 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; @@ -443,6 +441,12 @@ scrobble_cb(void *arg) } #endif +static void +pause_timer_cb(int fd, short what, void *arg) +{ + playback_abort(); +} + // 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 @@ -837,6 +841,7 @@ session_update_read_start(uint32_t seek_ms) 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; @@ -1015,7 +1020,8 @@ event_play_start() static inline void event_read(int nsamples) { - if (!pb_session.playing_now) // Shouldn't happen + // 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) @@ -1029,7 +1035,8 @@ event_read(int nsamples) } } - if (pb_session.playing_now->pos_ms == 0 && pb_session.pos + nsamples >= pb_session.playing_now->play_start) + // 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); @@ -1548,6 +1555,10 @@ playback_timer_start(void) struct itimerspec tick; int ret; + // The pause timer will be active if we have recently paused, but now that + // the playback loop has been kicked off, we no longer want that + event_del(player_pause_timeout_ev); + ret = event_add(pb_timer_ev, NULL); if (ret < 0) { @@ -1623,6 +1634,10 @@ playback_session_start(struct db_queue_item *queue_item, uint32_t seek_ms) if (!pb_session.playing_now) return -1; + // The input source is now open and ready, but we might actually be paused. So + // we activate the below event in case the user never starts us again + event_add(player_pause_timeout_ev, &player_pause_timeout); + return ret; } @@ -3150,7 +3165,6 @@ int player_init(void) { uint64_t interval; - uint32_t rnd; int ret; speaker_autoselect = cfg_getbool(cfg_getsec(cfg, "general"), "speaker_autoselect"); @@ -3161,7 +3175,7 @@ player_init(void) player_state = PLAY_STOPPED; repeat = REPEAT_OFF; - 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 @@ -3169,14 +3183,12 @@ player_init(void) 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", player_timer_res.tv_nsec); - player_timer_res.tv_nsec = 10 * PLAYER_TICK_INTERVAL * 1000000; } @@ -3197,54 +3209,37 @@ 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()); + CHECK_NULL(L_PLAYER, player_pause_timeout_ev = evtimer_new(evbase_player, pause_timer_cb, NULL)); #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"); @@ -3254,20 +3249,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; } From a37c9b4839efad897f475a385b9f4a94db230a55 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 22 Feb 2019 21:23:11 +0100 Subject: [PATCH 38/86] [player] Change name that was incorrect from merge conflict resolution --- src/player.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/player.c b/src/player.c index 6da93b61..8488e2a1 100644 --- a/src/player.c +++ b/src/player.c @@ -2235,7 +2235,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) From 16469f6fa3cf5684fe0c0555a4e44b13536a95db Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 22 Feb 2019 23:38:32 +0100 Subject: [PATCH 39/86] [player] Fixup missing fix from merge conflict resolution --- src/player.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/player.c b/src/player.c index 8488e2a1..3986ba41 100644 --- a/src/player.c +++ b/src/player.c @@ -2218,11 +2218,8 @@ speaker_enumerate(void *arg, int *retval) for (device = output_device_list; device; device = device->next) { - if (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; From 0f83b09ef7d2bba91e6ffe0605f7c24480d6da4c Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 23 Feb 2019 21:26:04 +0100 Subject: [PATCH 40/86] [player] Fix incorrect setting of start timestamp The time stamp was getting set too late, because if pos was zero the first reads then it would be overwritten, but it shouldn't because the loop will catch up even if the initial reads have zero samples. --- src/player.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/player.c b/src/player.c index 3986ba41..d647d798 100644 --- a/src/player.c +++ b/src/player.c @@ -852,7 +852,7 @@ static inline void session_update_read(int nsamples) { // Did we just complete our first read? Then set the start timestamp - if (pb_session.pos == 0) + if (pb_session.start_ts.tv_sec == 0) { clock_gettime_with_res(CLOCK_MONOTONIC, &pb_session.start_ts, &player_timer_res); pb_session.pts = pb_session.start_ts; @@ -1089,7 +1089,10 @@ source_read(int *nbytes, int *nsamples, struct media_quality *quality, uint8_t * } if (*nbytes == 0 || quality->channels == 0) - return 0; + { + event_read(0); // This will set start_ts even if source isn't open yet + return 0; + } *nsamples = BTOS(*nbytes, quality->bits_per_sample, quality->channels); From 143708368c6076042a5a385c12770b57c80c407c Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 23 Feb 2019 23:24:36 +0100 Subject: [PATCH 41/86] [alsa] Fix sync check Sync correction still not implemented --- forked-daapd.conf.in | 10 +-- src/conffile.c | 3 +- src/outputs/alsa.c | 160 ++++++++++++++++++++----------------------- 3 files changed, 83 insertions(+), 90 deletions(-) diff --git a/forked-daapd.conf.in b/forked-daapd.conf.in index f701a2fa..d85279bc 100644 --- a/forked-daapd.conf.in +++ b/forked-daapd.conf.in @@ -218,11 +218,11 @@ audio { # 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 + # If your local audio is out of sync with other speakers, e.g. Airplay, + # adjust this value. 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 # How often to check and correct for drift between ALSA and AirPlay. # The value is an integer expressed in seconds. diff --git a/src/conffile.c b/src/conffile.c index ca4bbbec..8f0585df 100644 --- a/src/conffile.c +++ b/src/conffile.c @@ -115,7 +115,8 @@ 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("offset", 0, CFGF_NONE), // deprecated + CFG_INT("offset_ms", 0, CFGF_NONE), CFG_INT("adjust_period_seconds", 10, CFGF_NONE), CFG_END() }; diff --git a/src/outputs/alsa.c b/src/outputs/alsa.c index c77e48fb..08e5d325 100644 --- a/src/outputs/alsa.c +++ b/src/outputs/alsa.c @@ -37,17 +37,13 @@ #include "player.h" #include "outputs.h" -// Same as Airplay - maybe not optimal? -#define ALSA_SAMPLES_PER_PACKET 352 -#define ALSA_PACKET_SIZE STOB(ALSA_SAMPLES_PER_PACKET, 16, 2) - // 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 +#define ALSA_MAX_LATENCY 480 // 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 +#define ALSA_MAX_LATENCY_VARIANCE 480 #define ALSA_F_STARTED (1 << 15) @@ -77,19 +73,20 @@ struct alsa_session int buffer_nsamp; uint32_t pos; - uint32_t start_pos; + + uint32_t last_pos; + uint32_t last_buflen; struct timespec start_pts; struct timespec last_pts; - snd_htimestamp_t dev_start_ts; - snd_pcm_sframes_t last_avail; - int32_t last_latency; + int last_latency; + int sync_counter; // Here we buffer samples during startup struct ringbuffer prebuf; - int offset; + int offset_ms; int adjust_period_seconds; int volume; @@ -456,24 +453,12 @@ device_close(struct alsa_session *as) } } -static inline void -device_timestamp(struct alsa_session *as, snd_pcm_sframes_t *delay, snd_pcm_sframes_t *avail, snd_htimestamp_t *ts) -{ - snd_pcm_status(as->hdl, as->pcm_status); - - if (delay) - *delay = snd_pcm_status_get_delay(as->pcm_status); - if (avail) - *avail = snd_pcm_status_get_avail(as->pcm_status); - - snd_pcm_status_get_htstamp(as->pcm_status, ts); -} - static void playback_restart(struct alsa_session *as, struct output_buffer *obuf) { + struct timespec ts; snd_pcm_state_t state; - snd_pcm_sframes_t delay; + snd_pcm_sframes_t offset_nsamp; size_t size; char *errmsg; int ret; @@ -519,25 +504,22 @@ playback_restart(struct alsa_session *as, struct output_buffer *obuf) // Clear prebuffer in case start got called twice without a stop in between ringbuffer_free(&as->prebuf, 1); - as->start_pos = 0; as->pos = 0; - // Time stamps used for syncing - as->start_pts = obuf->pts; + // 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->start_pts = timespec_add(obuf->pts, ts); - device_timestamp(as, &delay, &as->last_avail, &as->dev_start_ts); - if (as->dev_start_ts.tv_sec == 0 && as->dev_start_ts.tv_nsec == 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Can't get timestamps from ALSA, sync check is disabled\n"); - } + // 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); - // The difference between pos and start_pos should match the 2 second buffer - // that AirPlay uses (OUTPUTS_BUFFER_DURATION). 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. Delay might be - // non-zero if we are restarting (?). - as->buffer_nsamp = OUTPUTS_BUFFER_DURATION * as->quality.sample_rate - delay; + 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); @@ -603,60 +585,70 @@ buffer_write(struct alsa_session *as, struct output_data *odata, snd_pcm_sframes } static enum alsa_sync_state -sync_check(snd_pcm_sframes_t *delay, snd_pcm_sframes_t *avail, struct alsa_session *as, struct timespec pts) +sync_check(struct alsa_session *as) { enum alsa_sync_state sync; - snd_htimestamp_t ts; - uint64_t elapsed; - uint64_t dev_elapsed; - uint64_t pos; - uint64_t dev_pos; - uint32_t buffered_samples; + snd_pcm_sframes_t delay; + struct timespec ts; + int elapsed; + uint64_t cur_pos; + uint64_t exp_pos; int32_t latency; + int ret; - // We don't need avail for the sync check, but to reduce querying we retrieve - // it here as a service for the caller - device_timestamp(as, delay, avail, &ts); - if (ts.tv_sec == 0 && ts.tv_nsec == 0) + as->sync_counter++; + + ret = snd_pcm_delay(as->hdl, &delay); + if (ret < 0) return ALSA_SYNC_OK; - // Here we calculate elapsed time since we started, or since we last reset the - // sync timers: elapsed is how long the player thinks has elapsed, dev_elapsed - // is how long ALSA thinks has elapsed. If these are different, but the - // playback positition is the same, then the ALSA clock has drifted and we are - // coming out of sync. Unit is milliseconds. - elapsed = (pts.tv_sec - as->start_pts.tv_sec) * 1000L + (pts.tv_nsec - as->start_pts.tv_nsec) / 1000000; - dev_elapsed = (ts.tv_sec - as->dev_start_ts.tv_sec) * 1000L + (ts.tv_nsec - as->dev_start_ts.tv_nsec) / 1000000; + // 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); - // Now calculate playback positions. The pos is where we should be, dev_pos is - // where we actually are. - pos = as->start_pos + (elapsed - 1000 * OUTPUTS_BUFFER_DURATION) * as->quality.sample_rate / 1000; - buffered_samples = *delay + BTOS(as->prebuf.read_avail, as->quality.bits_per_sample, as->quality.channels); - dev_pos = as->start_pos + dev_elapsed * as->quality.sample_rate / 1000 - buffered_samples; + // Here we calculate elapsed time since playback was supposed to start, taking + // into account buffer time and configuration of offset_ms. We then calculate + // our expected position based on elapsed time, and if it is different from + // where we are + what is the buffers, then ALSA is out of sync. + elapsed = (ts.tv_sec - as->start_pts.tv_sec) * 1000L + (ts.tv_nsec - as->start_pts.tv_nsec) / 1000000; + if (elapsed < 0) + return ALSA_SYNC_OK; - // TODO calculate below and above more efficiently? - latency = pos - dev_pos; + cur_pos = (uint64_t)as->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; + latency = cur_pos - exp_pos; - // If the latency is low or very different from our last measurement, we will wait and see + // 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) - sync = ALSA_SYNC_OK; - else if (latency > 0) - sync = ALSA_SYNC_BEHIND; + { + 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 >= as->adjust_period_seconds) + { + as->sync_counter = 0; + if (latency < 0) + sync = ALSA_SYNC_BEHIND; + else + sync = ALSA_SYNC_AHEAD; + } else - sync = ALSA_SYNC_AHEAD; + sync = ALSA_SYNC_OK; // The will be used by sync_correct, so it knows how much we are out of sync as->last_latency = latency; - DPRINTF(E_DBG, L_LAUDIO, "Sync=%d, pos=%" PRIu64 ", as->pos=%u, dev_pos=%" PRIu64 ", latency=%d, delay=%li, avail=%li, elapsed=%" PRIu64 ", dev_elapsed=%" PRIu64 "\n", - sync, pos, as->pos, dev_pos, latency, *delay, *avail, elapsed / 1000000, dev_elapsed / 1000000); + DPRINTF(E_DBG, L_LAUDIO, "start %lu:%lu, now %lu:%lu, elapsed is %d ms, cur_pos=%" PRIu64 ", exp_pos=%" PRIu64 ", latency=%d\n", + as->start_pts.tv_sec, as->start_pts.tv_nsec / 1000000, ts.tv_sec, ts.tv_nsec / 1000000, elapsed, cur_pos, exp_pos, latency); return sync; } static void -sync_correct(void) +sync_correct(struct alsa_session *as) { + DPRINTF(E_INFO, L_LAUDIO, "Here we should take action to compensate for ALSA latency of %d samples\n", as->last_latency); // Not implemented yet } @@ -664,7 +656,7 @@ static void playback_write(struct alsa_session *as, struct output_buffer *obuf) { snd_pcm_sframes_t ret; - snd_pcm_sframes_t delay; + snd_pcm_sframes_t avail; enum alsa_sync_state sync; bool prebuffering; int i; @@ -692,17 +684,19 @@ playback_write(struct alsa_session *as, struct output_buffer *obuf) return; } - // Check sync each time a second has passed + // Check sync each second (or if this is first write where last_pts is zero) if (obuf->pts.tv_sec != as->last_pts.tv_sec) { - sync = sync_check(&delay, &as->last_avail, as, obuf->pts); + sync = sync_check(as); if (sync != ALSA_SYNC_OK) - sync_correct(); + sync_correct(as); as->last_pts = obuf->pts; } - ret = buffer_write(as, &obuf->data[i], as->last_avail); + avail = snd_pcm_avail(as->hdl); + + ret = buffer_write(as, &obuf->data[i], avail); if (ret < 0) goto alsa_error; @@ -798,15 +792,13 @@ alsa_session_make(struct output_device *device, int callback_id) if (!as->mixer_device_name || strlen(as->mixer_device_name) == 0) as->mixer_device_name = cfg_getstr(cfg_audio, "card"); - // TODO implement - as->offset = cfg_getint(cfg_audio, "offset"); - if (abs(as->offset) > 44100) + as->offset_ms = cfg_getint(cfg_audio, "offset_ms"); + if (abs(as->offset_ms) > 1000) { - DPRINTF(E_LOG, L_LAUDIO, "The ALSA offset (%d) set in the configuration is out of bounds\n", as->offset); - as->offset = 44100 * (as->offset/abs(as->offset)); + 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)); } - // TODO implement original_adjust = cfg_getint(cfg_audio, "adjust_period_seconds"); if (original_adjust < 1) as->adjust_period_seconds = 1; From 977f8570a5a784f34ce30995332edc7eecb9215d Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 24 Feb 2019 00:44:11 +0100 Subject: [PATCH 42/86] [player] Fix handling of underrun/read_deficit * Also call full_cb() from input_wait if buffer is full * Make read_deficit count missing bytes instead of clock ticks * Make read_deficit a part of the playback session --- src/input.c | 6 ++++++ src/player.c | 40 ++++++++++++++++++++++++---------------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/input.c b/src/input.c index ca6a3fcd..4de7360c 100644 --- a/src/input.c +++ b/src/input.c @@ -477,6 +477,12 @@ input_wait(void) // Is the buffer full? if (evbuffer_get_length(input_buffer.evbuf) > INPUT_BUFFER_THRESHOLD) { + if (input_buffer.full_cb) + { + input_buffer.full_cb(); + input_buffer.full_cb = NULL; + } + pthread_mutex_unlock(&input_buffer.mutex); return -1; } diff --git a/src/player.c b/src/player.c index d647d798..00e02af6 100644 --- a/src/player.c +++ b/src/player.c @@ -241,6 +241,11 @@ struct player_session // Equals current number of samples written to outputs uint32_t pos; + // 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; @@ -289,11 +294,7 @@ static struct timespec player_timer_res; static struct timeval player_pause_timeout = { PLAYER_PAUSE_TIME_MAX, 0 }; -// 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) @@ -879,6 +880,7 @@ session_update_read_quality(struct media_quality *quality) 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); @@ -1155,46 +1157,55 @@ playback_cb(int fd, short what, void *arg) 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. - for (i = 1 + overrun + pb_read_deficit; i > 0; i--) + for (i = 1 + overrun; i > 0; i--) { ret = source_read(&nbytes, &nsamples, &quality, 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) { - pb_read_deficit++; break; } + pb_session.read_deficit -= nbytes; + outputs_write(pb_session.buffer, pb_session.bufsize, &quality, nsamples, &pb_session.pts); if (nbytes < pb_session.bufsize) { - DPRINTF(E_DBG, L_PLAYER, "Incomplete read, wanted %zu, got %d\n", pb_session.bufsize, nbytes); + DPRINTF(E_DBG, L_PLAYER, "Incomplete read, wanted %zu, got %d, deficit %zu\n", pb_session.bufsize, nbytes, pb_session.read_deficit); // How much the number of samples we got corresponds to in time (nanoseconds) ts.tv_sec = 0; ts.tv_nsec = 1000000000L * nsamples / quality.sample_rate; pb_session.pts = timespec_add(pb_session.pts, ts); - pb_read_deficit++; } 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); - if (pb_read_deficit > 0) - pb_read_deficit--; + + // It is going well, lets take another round to repay our debt + if (i == 1 && pb_session.read_deficit > pb_session.bufsize) + i = 2; } } - if (pb_read_deficit > pb_read_deficit_max) + 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=%d)\n", pb_read_deficit); + 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); + playback_suspend(); } } @@ -1794,8 +1805,6 @@ playback_start_bh(void *arg, int *retval) { int ret; - pb_read_deficit = 0; - ret = playback_timer_start(); if (ret < 0) goto error; @@ -3197,7 +3206,6 @@ player_init(void) 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 From fc20b55a9b0538816c0d9a34523a3003d86bd91d Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 24 Feb 2019 14:33:25 +0100 Subject: [PATCH 43/86] [raop] Log sync packet times --- src/outputs/raop.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/outputs/raop.c b/src/outputs/raop.c index 7528e040..4c002e1a 100644 --- a/src/outputs/raop.c +++ b/src/outputs/raop.c @@ -2991,6 +2991,7 @@ 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 @@ -3006,6 +3007,8 @@ packets_sync_send(struct raop_master_session *rms, struct timespec pts) 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 @@ -3023,7 +3026,8 @@ packets_sync_send(struct raop_master_session *rms, struct timespec pts) 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 ", rtptime=%" PRIu32 "\n", rs->devname, cur_stamp.pos, rms->rtp_session->pos); + 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) { From 4017ce88464d0afe42aaa2dee8c21965eec2fe8b Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 24 Feb 2019 22:53:37 +0100 Subject: [PATCH 44/86] [player] Fixup to commit 8f39c65, avoid crash when queue ends --- src/player.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/player.c b/src/player.c index 00e02af6..21c1d0c7 100644 --- a/src/player.c +++ b/src/player.c @@ -1053,9 +1053,19 @@ source_read(int *nbytes, int *nsamples, struct media_quality *quality, uint8_t * { short flags; - // Nothing to read, stream silence until event_read() stops playback - if (!pb_session.reading_now && pb_session.playing_now) + // Nothing to read + if (!pb_session.reading_now) { + // This can happen if the loop tries to catch up with an overrun or a + // deficit, but the playback ends in the first iteration + if (!pb_session.playing_now) + { + *nbytes = 0; + *nsamples = 0; + return 0; + } + + // Stream silence until event_read() stops playback memset(buf, 0, len); *quality = pb_session.playing_now->quality; *nbytes = len; From 2893e597c36607f2e35b3f85776a7861d02d5967 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 25 Feb 2019 20:00:59 +0100 Subject: [PATCH 45/86] [player] Partially revert commit ec9633c, now don't do full stop straight away It is normal for the player to be called with stop() and then start(), so we should keep the outputs open for a while. --- src/player.c | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/player.c b/src/player.c index 21c1d0c7..581a065a 100644 --- a/src/player.c +++ b/src/player.c @@ -132,6 +132,12 @@ // (value is in seconds) #define PLAYER_PAUSE_TIME_MAX 600 +// 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 PLAYER_STOP_TIME_MAX 10 + //#define DEBUG_PLAYER 1 struct volume_param { @@ -285,7 +291,7 @@ static int pb_timer_fd; timer_t pb_timer; #endif static struct event *pb_timer_ev; -static struct event *player_pause_timeout_ev; +static struct event *player_abort_timeout_ev; // Time between ticks, i.e. time between when playback_cb() is invoked static struct timespec player_tick_interval; @@ -293,6 +299,7 @@ static struct timespec player_tick_interval; static struct timespec player_timer_res; static struct timeval player_pause_timeout = { PLAYER_PAUSE_TIME_MAX, 0 }; +static struct timeval player_stop_timeout = { PLAYER_STOP_TIME_MAX, 0 }; // PLAYER_WRITE_BEHIND_MAX converted to clock ticks static int pb_write_deficit_max; @@ -1581,7 +1588,7 @@ playback_timer_start(void) // The pause timer will be active if we have recently paused, but now that // the playback loop has been kicked off, we no longer want that - event_del(player_pause_timeout_ev); + event_del(player_abort_timeout_ev); ret = event_add(pb_timer_ev, NULL); if (ret < 0) @@ -1660,7 +1667,7 @@ playback_session_start(struct db_queue_item *queue_item, uint32_t seek_ms) // The input source is now open and ready, but we might actually be paused. So // we activate the below event in case the user never starts us again - event_add(player_pause_timeout_ev, &player_pause_timeout); + event_add(player_abort_timeout_ev, &player_pause_timeout); return ret; } @@ -1803,10 +1810,23 @@ playback_stop(void *arg, int *retval) 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); - playback_abort(); + // 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); + + playback_session_stop(); status_update(PLAY_STOPPED); + outputs_metadata_purge(); + + // In case we aren't restarting soon we want to make a full stop + event_add(player_abort_timeout_ev, &player_stop_timeout); + + // We're async if we need to flush devices + if (*retval > 0) + return COMMAND_PENDING; + return COMMAND_END; } @@ -3231,7 +3251,7 @@ player_init(void) } CHECK_NULL(L_PLAYER, evbase_player = event_base_new()); - CHECK_NULL(L_PLAYER, player_pause_timeout_ev = evtimer_new(evbase_player, pause_timer_cb, NULL)); + CHECK_NULL(L_PLAYER, player_abort_timeout_ev = evtimer_new(evbase_player, pause_timer_cb, NULL)); #ifdef HAVE_TIMERFD CHECK_NULL(L_PLAYER, pb_timer_ev = event_new(evbase_player, pb_timer_fd, EV_READ | EV_PERSIST, playback_cb, NULL)); #else From 50c7d96bc4ad82387982dd2eaaab229e2e769441 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 25 Feb 2019 20:04:23 +0100 Subject: [PATCH 46/86] [raop] Remove flush timer, the player now takes care of stopping Player now stops 10 secs after stop command and 10 mins after pause. At that time the outputs have probably cut the connection themselves, but that might be ok (needs testing). --- src/outputs/raop.c | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/outputs/raop.c b/src/outputs/raop.c index 4c002e1a..c55dc548 100644 --- a/src/outputs/raop.c +++ b/src/outputs/raop.c @@ -1939,7 +1939,7 @@ session_teardown_cb(struct evrtsp_request *req, void *arg) if (!req || 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 = OUTPUT_STATE_STOPPED; + rs->state = RAOP_STATE_STOPPED; raop_status(rs); @@ -4817,7 +4817,6 @@ static int raop_device_flush(struct output_device *device, int callback_id) { struct raop_session *rs = device->session; - struct timeval tv; int ret; if (rs->state != RAOP_STATE_STREAMING) @@ -4829,10 +4828,6 @@ raop_device_flush(struct output_device *device, int callback_id) rs->callback_id = callback_id; - evutil_timerclear(&tv); - tv.tv_sec = 10; - evtimer_add(rs->deferredev, &tv); - return 0; } @@ -4902,8 +4897,6 @@ raop_write(struct output_buffer *obuf) if (rs->state != RAOP_STATE_CONNECTED) continue; - event_del(rs->deferredev); // Kills flush timer in case playback was stopped but then restarted again - rs->state = RAOP_STATE_STREAMING; // Make a cb? } From 3c2ff294a1408e5441ab279b66ea0d611d29f40d Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 25 Feb 2019 22:47:37 +0100 Subject: [PATCH 47/86] [player] Fix rewind problem where quality level was lost by player + remember to flush in source_start(), since the input won't do it if input_now_reading has already been closed (e.g. if starting a new track while playback is at the end of another track) --- src/player.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/player.c b/src/player.c index 581a065a..8b349684 100644 --- a/src/player.c +++ b/src/player.c @@ -712,11 +712,15 @@ source_next(void) static int source_start(void) { + short flags; + if (!pb_session.reading_next) return 0; 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); + input_flush(&flags); + return input_seek(pb_session.reading_next->item_id, (int)pb_session.reading_next->seek_ms); } @@ -829,7 +833,7 @@ session_update_read_eof(void) return; // We inherit this because the input will only notify on quality changes, not - // if it the same as the previous track + // 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; @@ -1110,6 +1114,7 @@ source_read(int *nbytes, int *nsamples, struct media_quality *quality, uint8_t * if (*nbytes == 0 || quality->channels == 0) { event_read(0); // This will set start_ts even if source isn't open yet + event_read_quality(); // Will poll input for quality since we don't have it return 0; } From 63a2e750c75c5cb43ea176790d5bff10d12ad41a Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 26 Feb 2019 23:04:16 +0100 Subject: [PATCH 48/86] [pulse] Convert Pulseaudio to new interface, incl support for native quality First draft, probably has a few bugs --- src/outputs/pulse.c | 278 +++++++++++++++++++++++++++----------------- 1 file changed, 174 insertions(+), 104 deletions(-) diff --git a/src/outputs/pulse.c b/src/outputs/pulse.c index 8afd40c0..ed49e809 100644 --- a/src/outputs/pulse.c +++ b/src/outputs/pulse.c @@ -38,8 +38,6 @@ #include "outputs.h" #include "commands.h" -// From Airplay -#define PULSE_SAMPLES_PER_PACKET 352 #define PULSE_MAX_DEVICES 64 #define PULSE_LOG_MAX 10 @@ -60,19 +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; - - struct output_device *device; - 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,28 +141,36 @@ pulse_session_cleanup(struct pulse_session *ps) p->next = ps->next; } - ps->device->session = NULL; + 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 pulse_session *ps; + int ret; + + ret = outputs_quality_subscribe(&pulse_fallback_quality); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not subscribe to fallback audio quality\n"); + return NULL; + } CHECK_NULL(L_LAUDIO, ps = calloc(1, sizeof(struct pulse_session))); 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, ps); + outputs_device_session_add(device->id, ps); return ps; } @@ -173,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) @@ -196,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 } @@ -557,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); @@ -587,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 + PULSE_SAMPLES_PER_PACKET - offset, 16, 2); // 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; @@ -610,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); @@ -620,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); @@ -634,39 +654,121 @@ 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; } @@ -704,18 +806,18 @@ pulse_device_flush(struct output_device *device, int callback_id) } static int -pulse_device_probe(struct output_device *device, output_status_cb cb) +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); @@ -731,18 +833,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); @@ -752,7 +861,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) @@ -769,63 +878,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(PULSE_SAMPLES_PER_PACKET, 16, 2); - - 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 @@ -858,14 +937,6 @@ pulse_playback_stop(void) pa_threaded_mainloop_unlock(pulse.mainloop); } -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 pulse_init(void) { @@ -959,10 +1030,9 @@ struct output_definition output_pulse = .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, - .status_cb = pulse_set_status_cb, }; From 8cf717664dfa9e2c938b8ad23a1aacbea41aa16d Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 26 Feb 2019 23:07:02 +0100 Subject: [PATCH 49/86] [player] Don't call event_read_quality() twice --- src/player.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/player.c b/src/player.c index 8b349684..cf31ac96 100644 --- a/src/player.c +++ b/src/player.c @@ -1106,7 +1106,7 @@ source_read(int *nbytes, int *nsamples, struct media_quality *quality, uint8_t * { event_metadata_new(); } - else if (flags & INPUT_FLAG_QUALITY) + else if (flags & INPUT_FLAG_QUALITY || quality->channels == 0) { event_read_quality(); } @@ -1114,7 +1114,6 @@ source_read(int *nbytes, int *nsamples, struct media_quality *quality, uint8_t * if (*nbytes == 0 || quality->channels == 0) { event_read(0); // This will set start_ts even if source isn't open yet - event_read_quality(); // Will poll input for quality since we don't have it return 0; } From 39037c7c8b93bc91bde542f7e7671fe68a479cc8 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Wed, 27 Feb 2019 20:32:22 +0100 Subject: [PATCH 50/86] [player] Fix typo --- src/player.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/player.c b/src/player.c index cf31ac96..27132f29 100644 --- a/src/player.c +++ b/src/player.c @@ -903,7 +903,7 @@ session_update_read_quality(struct media_quality *quality) CHECK_NULL(L_PLAYER, pb_session.buffer); - pb_session.reading_now->play_start = pb_session.reading_now->play_start + pb_session.reading_now->output_buffer_samples; + pb_session.reading_now->play_start = pb_session.reading_now->read_start + pb_session.reading_now->output_buffer_samples; } static void From 1285128b9ab831e0485e17de2c663dfaecab050d Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Wed, 27 Feb 2019 21:58:33 +0100 Subject: [PATCH 51/86] [xcode] Fix incorrect source BPS detection + support for more audio qualities --- src/transcode.c | 64 +++++++++++++++++++++++++++++++++++++++++++++---- src/transcode.h | 5 ++++ 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/transcode.c b/src/transcode.c index 11831822..6de7b51d 100644 --- a/src/transcode.c +++ b/src/transcode.c @@ -216,6 +216,16 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile) settings->sample_format = AV_SAMPLE_FMT_S16; break; + case XCODE_PCM16_96000: + settings->encode_audio = 1; + settings->format = "s16le"; + settings->audio_codec = AV_CODEC_ID_PCM_S16LE; + settings->sample_rate = 96000; + settings->channel_layout = AV_CH_LAYOUT_STEREO; + settings->channels = 2; + settings->sample_format = AV_SAMPLE_FMT_S16; + break; + case XCODE_PCM24_44100: settings->encode_audio = 1; settings->format = "s24le"; @@ -236,6 +246,46 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile) settings->sample_format = AV_SAMPLE_FMT_S32; break; + case XCODE_PCM24_96000: + settings->encode_audio = 1; + settings->format = "s24le"; + settings->audio_codec = AV_CODEC_ID_PCM_S24LE; + settings->sample_rate = 96000; + settings->channel_layout = AV_CH_LAYOUT_STEREO; + settings->channels = 2; + settings->sample_format = AV_SAMPLE_FMT_S32; + break; + + case XCODE_PCM32_44100: + settings->encode_audio = 1; + settings->format = "s32le"; + settings->audio_codec = AV_CODEC_ID_PCM_S32LE; + settings->sample_rate = 44100; + settings->channel_layout = AV_CH_LAYOUT_STEREO; + settings->channels = 2; + settings->sample_format = AV_SAMPLE_FMT_S32; + break; + + case XCODE_PCM32_48000: + settings->encode_audio = 1; + settings->format = "s32le"; + settings->audio_codec = AV_CODEC_ID_PCM_S32LE; + settings->sample_rate = 48000; + settings->channel_layout = AV_CH_LAYOUT_STEREO; + settings->channels = 2; + settings->sample_format = AV_SAMPLE_FMT_S32; + break; + + case XCODE_PCM32_96000: + settings->encode_audio = 1; + settings->format = "s32le"; + settings->audio_codec = AV_CODEC_ID_PCM_S32LE; + settings->sample_rate = 96000; + settings->channel_layout = AV_CH_LAYOUT_STEREO; + settings->channels = 2; + settings->sample_format = AV_SAMPLE_FMT_S32; + break; + case XCODE_MP3: settings->encode_audio = 1; settings->format = "mp3"; @@ -1198,17 +1248,19 @@ transcode_encode_setup(enum transcode_profile profile, struct decode_ctx *src_ct ctx->settings.width = width; ctx->settings.height = height; + // Profile does 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; + // Profile does not specify a sample format -> use same as source if (!ctx->settings.sample_format && ctx->settings.encode_audio) { - bps = av_get_bits_per_sample(src_ctx->audio_stream.codec->codec_id); - if (bps >= 24) + 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_S24LE; - ctx->settings.format = "s24le"; + ctx->settings.audio_codec = AV_CODEC_ID_PCM_S32LE; + ctx->settings.format = "s32le"; } else { @@ -1582,6 +1634,10 @@ transcode_frame_new(void *data, size_t size, int nsamples, int sample_rate, int { f->format = AV_SAMPLE_FMT_S32; } + else if (bits_per_sample == 32) + { + f->format = AV_SAMPLE_FMT_S32; + } else { DPRINTF(E_LOG, L_XCODE, "transcode_frame_new() called with unsupported bps (%d)\n", bits_per_sample); diff --git a/src/transcode.h b/src/transcode.h index 3ce05d26..3db09ae2 100644 --- a/src/transcode.h +++ b/src/transcode.h @@ -17,8 +17,13 @@ enum transcode_profile // Decodes/resamples the best audio stream (no wav headers) XCODE_PCM16_44100, XCODE_PCM16_48000, + XCODE_PCM16_96000, XCODE_PCM24_44100, XCODE_PCM24_48000, + XCODE_PCM24_96000, + XCODE_PCM32_44100, + XCODE_PCM32_48000, + XCODE_PCM32_96000, // Transcodes the best audio stream into MP3 XCODE_MP3, // Transcodes the best audio stream into OPUS From 3e4766ffaaa3716b41a3506f735ef1f3c31a2f8c Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Wed, 27 Feb 2019 21:59:44 +0100 Subject: [PATCH 52/86] [outputs] Fix segfault if encode_reset() fails to setup an encode_ctx --- src/outputs.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/outputs.c b/src/outputs.c index 6bde6c51..f1f5566f 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -223,10 +223,20 @@ quality_to_xcode(struct media_quality *quality) return XCODE_PCM16_44100; if (quality->sample_rate == 44100 && quality->bits_per_sample == 24) return XCODE_PCM24_44100; + if (quality->sample_rate == 44100 && quality->bits_per_sample == 32) + return XCODE_PCM32_44100; if (quality->sample_rate == 48000 && quality->bits_per_sample == 16) return XCODE_PCM16_48000; if (quality->sample_rate == 48000 && quality->bits_per_sample == 24) return XCODE_PCM24_48000; + if (quality->sample_rate == 48000 && quality->bits_per_sample == 32) + return XCODE_PCM32_48000; + if (quality->sample_rate == 96000 && quality->bits_per_sample == 16) + return XCODE_PCM16_96000; + if (quality->sample_rate == 96000 && quality->bits_per_sample == 24) + return XCODE_PCM24_96000; + if (quality->sample_rate == 96000 && quality->bits_per_sample == 32) + return XCODE_PCM32_96000; return XCODE_UNKNOWN; } @@ -310,6 +320,9 @@ buffer_fill(struct output_buffer *obuf, void *buf, size_t bufsize, struct media_ 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->sample_rate, quality->bits_per_sample); if (!frame) continue; From d762bd0a56154eba5d6e8f8943f124805a328b18 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 28 Feb 2019 23:17:47 +0100 Subject: [PATCH 53/86] [raop] master_session_free should allow null as argument --- src/outputs/raop.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/outputs/raop.c b/src/outputs/raop.c index c55dc548..8fccb4c5 100644 --- a/src/outputs/raop.c +++ b/src/outputs/raop.c @@ -1805,6 +1805,9 @@ master_session_make(struct media_quality *quality, bool encrypt) static void 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); From 9b6a892984ebef243b0c240c18d5ada1437e5cf7 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 28 Feb 2019 23:18:42 +0100 Subject: [PATCH 54/86] [rtp] Extra commenting about the RTP header --- src/outputs/rtp_common.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/outputs/rtp_common.c b/src/outputs/rtp_common.c index 20074608..5a7a4074 100644 --- a/src/outputs/rtp_common.c +++ b/src/outputs/rtp_common.c @@ -156,7 +156,17 @@ rtp_packet_next(struct rtp_session *session, size_t payload_len, int samples, ch pkt->data_len = RTP_HEADER_LEN + payload_len; pkt->seqnum = session->seqnum; - // RTP Header + + // 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 From f8b0147b12c6141fe5a8b939ee696128d6325f56 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 28 Feb 2019 23:19:05 +0100 Subject: [PATCH 55/86] [cast] First draft of Chromecast based on RTP (instead of mp3) Includes adaptions to modified outputs interface --- src/outputs/cast.c | 816 +++++++++++++++++++++++++++++++++------------ 1 file changed, 610 insertions(+), 206 deletions(-) diff --git a/src/outputs/cast.c b/src/outputs/cast.c index 5323e809..7ee40ffd 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,22 @@ #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 "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 +88,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 +118,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 +134,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 +147,51 @@ 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, + // After OFFER + CAST_STATE_MEDIA_STREAMING = CAST_STATE_F_MEDIA_CONNECTED | CAST_STATE_F_MEDIA_STREAMING, + +/* // 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, + CAST_STATE_MEDIA_BUFFERING = CAST_STATE_F_MEDIA_CONNECTED | CAST_STATE_F_MEDIA_LOADED | CAST_STATE_F_MEDIA_STREAMING, // After PLAY - CAST_STATE_MEDIA_PLAYING = CAST_STATE_F_MEDIA_CONNECTED | CAST_STATE_F_MEDIA_LOADED | CAST_STATE_F_MEDIA_PLAYING | 0x01, + CAST_STATE_MEDIA_PLAYING = CAST_STATE_F_MEDIA_CONNECTED | CAST_STATE_F_MEDIA_LOADED | CAST_STATE_F_MEDIA_STREAMING | 0x01, +*/ +}; + +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 +199,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,13 +229,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; - struct output_device *device; - output_status_cb status_cb; + int udp_fd; + unsigned short udp_port; struct cast_session *next; }; @@ -196,6 +253,8 @@ enum cast_msg_types STOP, MEDIA_CONNECT, MEDIA_CLOSE, + OFFER, + ANSWER, MEDIA_GET_STATUS, MEDIA_STATUS, MEDIA_LOAD, @@ -225,7 +284,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. @@ -294,6 +355,20 @@ 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 + .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, @@ -352,29 +427,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) { @@ -423,7 +500,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); @@ -516,17 +593,49 @@ 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; + } + + 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); @@ -540,8 +649,6 @@ cast_session_free(struct cast_session *cs) if (cs->transport_id) free(cs->transport_id); - free(cs->output_session); - free(cs); } @@ -550,11 +657,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) @@ -563,6 +670,8 @@ cast_session_cleanup(struct cast_session *cs) s->next = cs->next; } + outputs_device_session_remove(cs->device_id); + cast_session_free(cs); } @@ -616,6 +725,8 @@ 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 == 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)) @@ -692,6 +803,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)) @@ -823,7 +945,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)) { @@ -851,7 +981,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) @@ -868,10 +997,14 @@ 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: +/* 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_BUFFERING ... CAST_STATE_MEDIA_STREAMING: + state = OUTPUT_STATE_STREAMING; + break; +*/ + case CAST_STATE_MEDIA_STREAMING: state = OUTPUT_STATE_STREAMING; break; default: @@ -879,9 +1012,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 */ @@ -928,6 +1060,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) { @@ -944,12 +1117,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: @@ -1073,28 +1244,35 @@ cast_cb_probe(struct cast_session *cs, struct cast_msg_payload *payload) } /* cast_cb_load: Callback from starting playback */ -static void +/*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); + DPRINTF(E_LOG, L_CAST, "No reply from '%s' to our OFFER request\n", cs->devname); goto error; } - else if ((payload->type == MEDIA_LOAD_FAILED) || (payload->type == MEDIA_LOAD_CANCELLED)) + else if ((payload->type != ANSWER)) { - DPRINTF(E_LOG, L_CAST, "The device '%s' could not start playback\n", cs->devname); + 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->media_session_id) +// TODO check result == "ok" + else if (!payload->udp_port) { - DPRINTF(E_LOG, L_CAST, "Missing media session id in MEDIA_STATUS - aborting\n"); + DPRINTF(E_LOG, L_CAST, "Missing UDP port in ANSWER - aborting\n"); goto error; } - cs->media_session_id = payload->media_session_id; - // We autoplay for the time being - cs->state = CAST_STATE_MEDIA_PLAYING; + DPRINTF(E_LOG, 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; + + cs->state = CAST_STATE_MEDIA_LOADED; cast_status(cs); @@ -1103,7 +1281,7 @@ cast_cb_load(struct cast_session *cs, struct cast_msg_payload *payload) error: cast_session_shutdown(cs, CAST_STATE_FAILED); } - +*/ static void cast_cb_volume(struct cast_session *cs, struct cast_msg_payload *payload) { @@ -1118,7 +1296,7 @@ cast_cb_flush(struct cast_session *cs, struct cast_msg_payload *payload) 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; + cs->state = CAST_STATE_MEDIA_CONNECTED; cast_status(cs); } @@ -1138,7 +1316,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; @@ -1243,7 +1421,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)); @@ -1259,13 +1436,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) { @@ -1299,6 +1469,8 @@ cast_device_cb(const char *name, const char *type, const char *domain, const cha DPRINTF(E_INFO, L_CAST, "Adding Chromecast device '%s'\n", name); + device->advertised = 1; + switch (family) { case AF_INET: @@ -1318,10 +1490,49 @@ cast_device_cb(const char *name, const char *type, const char *domain, const cha /* --------------------- 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_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, output_status_cb cb) +cast_session_make(struct output_device *device, int family, int callback_id) { struct cast_session *cs; const char *proto; @@ -1357,8 +1568,17 @@ cast_session_make(struct output_device *device, int family, output_status_cb cb) CHECK_NULL(L_CAST, cs = calloc(1, sizeof(struct cast_session))); 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) || @@ -1366,10 +1586,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); @@ -1408,18 +1628,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 session for the device - outputs_device_session_add(device, 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)); @@ -1431,9 +1654,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); @@ -1464,12 +1689,15 @@ 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_LOADED ... CAST_STATE_MEDIA_STREAMING: + 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)) @@ -1486,7 +1714,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; @@ -1526,20 +1754,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->sample_rate, quality->bits_per_sample); + if (!frame) + { + DPRINTF(E_LOG, L_CAST, "Could not convert raw PCM to frame (bufsize=%ld)\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) { @@ -1550,13 +1965,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) { @@ -1568,66 +1983,64 @@ 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; + int ret; + + if (!(cs->state & CAST_STATE_F_MEDIA_STREAMING)) + return -1; + + // TODO Can't do this, we need to pause the stream in some other way + ret = cast_msg_send(cs, MEDIA_PAUSE, cast_cb_flush); + if (ret < 0) + return -1; + + cs->callback_id = callback_id; + + 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; @@ -1640,34 +2053,18 @@ 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) -{ - struct cast_session *cs; - - if (evtimer_pending(flush_timer, NULL)) - event_del(flush_timer); - - // TODO Maybe we could avoid reloading and instead support play->pause->play - for (cs = sessions; cs; cs = cs->next) - { - 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) + for (cs = cast_sessions; cs; cs = next) { next = cs->next; if (cs->state & CAST_STATE_F_MEDIA_CONNECTED) @@ -1676,57 +2073,55 @@ cast_playback_stop(void) } 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) +cast_write(struct output_buffer *obuf) { + struct cast_master_session *cms; struct cast_session *cs; - struct cast_session *next; - int pending; - int ret; + int i; - pending = 0; - for (cs = sessions; cs; cs = next) + if (!cast_sessions) + return; + + cms = cast_master_session; + + for (i = 0; obuf->data[i].buffer; i++) { - 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; -} - -static void -cast_set_status_cb(struct output_session *session, output_status_cb cb) -{ - struct cast_session *cs = session->session; - - cs->status_cb = cb; + cs->state = CAST_STATE_MEDIA_STREAMING; + // Make a cb? + } } static int cast_init(void) { + struct decode_ctx *decode_ctx; int family; int i; int ret; @@ -1751,10 +2146,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_48000); + 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, 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; } @@ -1767,13 +2170,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(); @@ -1786,13 +2191,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(); @@ -1804,18 +2210,16 @@ struct output_definition output_cast = .type = OUTPUT_TYPE_CAST, .priority = 2, .disabled = 0, + .device_start = cast_device_start, + .device_probe = cast_device_probe, + .device_stop = cast_device_stop, + .device_flush = cast_device_flush, + .device_cb_set = cast_device_cb_set, + .device_volume_set = cast_device_volume_set, + .playback_stop = cast_playback_stop, + .write = cast_write, .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, From 01797662decd0af64a584ea0a7c3eb5bdc061145 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 2 Mar 2019 00:30:43 +0100 Subject: [PATCH 56/86] [cast] Fix segfault from cast_master_session not being reset --- src/outputs/cast.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/outputs/cast.c b/src/outputs/cast.c index 7ee40ffd..eabeadfc 100644 --- a/src/outputs/cast.c +++ b/src/outputs/cast.c @@ -361,7 +361,8 @@ struct cast_msg_basic cast_msg[] = // 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 + // 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, }, @@ -618,6 +619,9 @@ master_session_cleanup(struct cast_master_session *cms) return; } + if (cms == cast_master_session) + cast_master_session = NULL; + master_session_free(cms); } From 9b31264a3c759a1c3365036f16e057a4e8d38f30 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 2 Mar 2019 16:43:32 +0100 Subject: [PATCH 57/86] [xcode] Use av_get_bytes_per_sample instead of av_get_bits_per_sample av_get_bits_per_sample(ctx->settings.audio_codec) may return 0 with some codecs --- src/transcode.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/transcode.c b/src/transcode.c index 6de7b51d..23ceb353 100644 --- a/src/transcode.c +++ b/src/transcode.c @@ -387,8 +387,8 @@ make_wav_header(struct encode_ctx *ctx, struct decode_ctx *src_ctx, off_t *est_s else duration = 3 * 60 * 1000; /* 3 minutes, in ms */ - bps = av_get_bits_per_sample(ctx->settings.audio_codec); - wav_len = ctx->settings.channels * (bps / 8) * 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); if (est_size) *est_size = wav_len + sizeof(ctx->header); @@ -400,9 +400,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 * (bps / 8)); /* byte rate */ - add_le16(ctx->header + 32, ctx->settings.channels * (bps / 8)); /* block align */ - add_le16(ctx->header + 34, bps); /* 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); } @@ -1281,8 +1281,8 @@ transcode_encode_setup(enum transcode_profile profile, struct decode_ctx *src_ct if (ctx->settings.icy && src_ctx->data_kind == DATA_KIND_HTTP) { - bps = av_get_bits_per_sample(ctx->settings.audio_codec); - ctx->icy_interval = METADATA_ICY_INTERVAL * ctx->settings.channels * (bps / 8) * 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; From daf7d755eba6f62bb8362344f90a60036f7699e7 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 2 Mar 2019 16:44:29 +0100 Subject: [PATCH 58/86] [input] Fix input writing too slowly for buffer to fill up after underrun After an underrun the player doesn't read, so that meant input_wait would wait a second before allowing the input to write, even though the input_buffer was not full --- src/input.c | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/input.c b/src/input.c index 4de7360c..b418d049 100644 --- a/src/input.c +++ b/src/input.c @@ -471,10 +471,7 @@ input_wait(void) pthread_mutex_lock(&input_buffer.mutex); - ts = timespec_reltoabs(input_loop_timeout); - pthread_cond_timedwait(&input_buffer.cond, &input_buffer.mutex, &ts); - - // Is the buffer full? + // 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) { if (input_buffer.full_cb) @@ -483,8 +480,14 @@ input_wait(void) input_buffer.full_cb = NULL; } - pthread_mutex_unlock(&input_buffer.mutex); - return -1; + 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); @@ -597,7 +600,7 @@ input_read(void *data, size_t size, short *flags) if (*flags || (debug_elapsed > 10 * one_sec_size)) { debug_elapsed = 0; - DPRINTF(E_SPAM, L_PLAYER, "READ %zu bytes (%d/%d/%d), WROTE %zu bytes (%d/%d/%d), SIZE %zu (=%zu), FLAGS %04x\n", + 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, From f7096890f60cc6cf064a02a167738beb23826537 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 3 Mar 2019 00:14:22 +0100 Subject: [PATCH 59/86] [player] Fix incorrect calculation of timestamp due to integer overflow --- src/player.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/player.c b/src/player.c index 27132f29..309ccd37 100644 --- a/src/player.c +++ b/src/player.c @@ -1205,10 +1205,12 @@ playback_cb(int fd, short what, void *arg) if (nbytes < pb_session.bufsize) { - DPRINTF(E_DBG, L_PLAYER, "Incomplete read, wanted %zu, got %d, deficit %zu\n", pb_session.bufsize, nbytes, pb_session.read_deficit); // How much the number of samples we got corresponds to in time (nanoseconds) ts.tv_sec = 0; - ts.tv_nsec = 1000000000L * nsamples / quality.sample_rate; + ts.tv_nsec = 1000000000UL * (uint64_t)nsamples / 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 @@ -1818,6 +1820,7 @@ playback_stop(void *arg, int *retval) // stop just yet; this saves time when restarting, which is nicer for the user *retval = outputs_flush(device_command_cb); + // Stops the input playback_session_stop(); status_update(PLAY_STOPPED); From e3d39cff9b364db379da354864453672123bc73f Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 3 Mar 2019 18:17:57 +0100 Subject: [PATCH 60/86] [player] Fix incorrect setting of buffer size to outputs --- src/outputs.c | 2 +- src/outputs.h | 2 +- src/player.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/outputs.c b/src/outputs.c index f1f5566f..22fbb7ed 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -815,7 +815,7 @@ outputs_flush(output_status_cb cb) } void -outputs_write(void *buf, size_t bufsize, struct media_quality *quality, int nsamples, struct timespec *pts) +outputs_write(void *buf, size_t bufsize, int nsamples, struct media_quality *quality, struct timespec *pts) { int i; diff --git a/src/outputs.h b/src/outputs.h index 49e62892..ccc338ad 100644 --- a/src/outputs.h +++ b/src/outputs.h @@ -309,7 +309,7 @@ int outputs_flush(output_status_cb cb); void -outputs_write(void *buf, size_t bufsize, struct media_quality *quality, int nsamples, struct timespec *pts); +outputs_write(void *buf, size_t bufsize, int nsamples, struct media_quality *quality, struct timespec *pts); struct output_metadata * outputs_metadata_prepare(int id); diff --git a/src/player.c b/src/player.c index 309ccd37..47232e44 100644 --- a/src/player.c +++ b/src/player.c @@ -1201,7 +1201,7 @@ playback_cb(int fd, short what, void *arg) pb_session.read_deficit -= nbytes; - outputs_write(pb_session.buffer, pb_session.bufsize, &quality, nsamples, &pb_session.pts); + outputs_write(pb_session.buffer, nbytes, nsamples, &quality, &pb_session.pts); if (nbytes < pb_session.bufsize) { From b8e0280567ffc9a578f53a7ceab6858f4851b62e Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 5 Mar 2019 22:45:38 +0100 Subject: [PATCH 61/86] [outputs] Add a 10 sec stop timer + drop playback_stop() In the output implementations playback_stop() was somewhat redundant, since device_stop() does the same. The timer should make sure that we always close outputs (previously they were in some cases kept open). The commit also includes some renaming. --- src/outputs.c | 160 ++++++++++++++++++++++++++++++------------- src/outputs.h | 18 +++-- src/outputs/alsa.c | 17 ----- src/outputs/cast.c | 15 ----- src/outputs/dummy.c | 13 ---- src/outputs/fifo.c | 15 ----- src/outputs/pulse.c | 32 --------- src/outputs/raop.c | 18 ++--- src/player.c | 161 ++++++++++++++++++++++++++------------------ 9 files changed, 226 insertions(+), 223 deletions(-) diff --git a/src/outputs.c b/src/outputs.c index 22fbb7ed..c084f3ec 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -74,9 +74,15 @@ static struct output_definition *outputs[] = { NULL }; +// 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_queue +struct outputs_callback_register { output_status_cb cb; struct output_device *device; @@ -89,9 +95,6 @@ struct outputs_callback_queue enum output_device_state state; }; -struct outputs_callback_queue outputs_cb_queue[OUTPUTS_MAX_CALLBACKS]; -struct event *outputs_deferredev; - struct output_quality_subscription { int count; @@ -99,13 +102,31 @@ struct output_quality_subscription 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 output_got_new_subscription; +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) { @@ -114,12 +135,12 @@ callback_remove(struct output_device *device) if (!device) return; - for (callback_id = 0; callback_id < ARRAY_SIZE(outputs_cb_queue); callback_id++) + for (callback_id = 0; callback_id < ARRAY_SIZE(outputs_cb_register); callback_id++) { - if (outputs_cb_queue[callback_id].device == device) + if (outputs_cb_register[callback_id].device == device) { - DPRINTF(E_DBG, L_PLAYER, "Removing callback to %s, id %d\n", player_pmap(outputs_cb_queue[callback_id].cb), callback_id); - memset(&outputs_cb_queue[callback_id], 0, sizeof(struct outputs_callback_queue)); + 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)); } } } @@ -137,26 +158,26 @@ callback_add(struct output_device *device, output_status_cb cb) callback_remove(device); // Find a free slot in the queue - for (callback_id = 0; callback_id < ARRAY_SIZE(outputs_cb_queue); callback_id++) + for (callback_id = 0; callback_id < ARRAY_SIZE(outputs_cb_register); callback_id++) { - if (outputs_cb_queue[callback_id].cb == NULL) + if (outputs_cb_register[callback_id].cb == NULL) break; } - if (callback_id == ARRAY_SIZE(outputs_cb_queue)) + 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_queue[callback_id].cb = cb; - outputs_cb_queue[callback_id].device = device; // Don't dereference this later, it might become invalid! + 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_queue); i++) - if (outputs_cb_queue[i].cb) + 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); @@ -172,19 +193,19 @@ deferred_cb(int fd, short what, void *arg) enum output_device_state state; int callback_id; - for (callback_id = 0; callback_id < ARRAY_SIZE(outputs_cb_queue); callback_id++) + for (callback_id = 0; callback_id < ARRAY_SIZE(outputs_cb_register); callback_id++) { - if (outputs_cb_queue[callback_id].ready) + 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_queue[callback_id].cb; - state = outputs_cb_queue[callback_id].state; + 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_queue[callback_id].device_id); + device = outputs_device_get(outputs_cb_register[callback_id].device_id); - memset(&outputs_cb_queue[callback_id], 0, sizeof(struct outputs_callback_queue)); + 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 @@ -200,13 +221,22 @@ deferred_cb(int fd, short what, void *arg) } } - for (int i = 0; i < ARRAY_SIZE(outputs_cb_queue); i++) + for (int i = 0; i < ARRAY_SIZE(outputs_cb_register); i++) { - if (outputs_cb_queue[i].cb) - DPRINTF(E_DBG, L_PLAYER, "%d. Active callback: %s\n", i, player_pmap(outputs_cb_queue[i].cb)); + 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) { @@ -300,10 +330,10 @@ buffer_fill(struct output_buffer *obuf, void *buf, size_t bufsize, struct media_ // 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) || output_got_new_subscription) + if (!quality_is_equal(quality, &obuf->data[0].quality) || outputs_got_new_subscription) { encoding_reset(quality); - output_got_new_subscription = false; + outputs_got_new_subscription = false; } // The first element of the output_buffer is always just the raw input data @@ -468,7 +498,7 @@ outputs_quality_subscribe(struct media_quality *quality) quality->sample_rate, quality->bits_per_sample, quality->channels, output_quality_subscriptions[i].count); // Better way of signaling this? - output_got_new_subscription = true; + outputs_got_new_subscription = true; return 0; } @@ -515,7 +545,7 @@ 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_queue)) || !outputs_cb_queue[callback_id].cb) + 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; @@ -523,9 +553,9 @@ outputs_cb(int callback_id, uint64_t device_id, enum output_device_state state) DPRINTF(E_DBG, L_PLAYER, "Callback request received, id is %i\n", callback_id); - outputs_cb_queue[callback_id].ready = true; - outputs_cb_queue[callback_id].device_id = device_id; - outputs_cb_queue[callback_id].state = state; + 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); } @@ -558,6 +588,8 @@ outputs_device_add(struct output_device *add, bool new_deselect, int default_vol { 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) @@ -691,6 +723,19 @@ outputs_device_stop(struct output_device *device, output_status_cb cb) 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; + + 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) { @@ -772,6 +817,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); @@ -780,21 +828,6 @@ outputs_device_free(struct output_device *device) free(device); } -void -outputs_playback_stop(void) -{ - int i; - - for (i = 0; outputs[i]; i++) - { - if (outputs[i]->disabled) - continue; - - if (outputs[i]->playback_stop) - outputs[i]->playback_stop(); - } -} - int outputs_flush(output_status_cb cb) { @@ -814,6 +847,39 @@ outputs_flush(output_status_cb cb) return count; } +int +outputs_stop(output_status_cb cb) +{ + struct output_device *device; + int count = 0; + int ret; + + for (device = output_device_list; device; device = device->next) + { + if (!device->session) + continue; + + ret = outputs_device_stop(device, cb); + if (ret < 0) + continue; + + 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(void *buf, size_t bufsize, int nsamples, struct media_quality *quality, struct timespec *pts) { diff --git a/src/outputs.h b/src/outputs.h index ccc338ad..e1b5d71e 100644 --- a/src/outputs.h +++ b/src/outputs.h @@ -3,6 +3,7 @@ #define __OUTPUTS_H__ #include +#include #include #include "misc.h" @@ -137,6 +138,8 @@ struct output_device short v4_port; short v6_port; + struct event *stop_timer; + // Opaque pointers to device and session data void *extra_device_info; void *session; @@ -220,9 +223,6 @@ struct output_definition // Free the private device data void (*device_free_extra)(struct output_device *device); - // Start/stop playback on devices that were started - void (*playback_stop)(void); - // Write stream data to the output devices void (*write)(struct output_buffer *buffer); @@ -281,6 +281,9 @@ 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); @@ -302,12 +305,15 @@ outputs_device_cb_set(struct output_device *device, output_status_cb cb); void outputs_device_free(struct output_device *device); -void -outputs_playback_stop(void); - int outputs_flush(output_status_cb cb); +int +outputs_stop(output_status_cb cb); + +int +outputs_stop_delayed_cancel(void); + void outputs_write(void *buf, size_t bufsize, int nsamples, struct media_quality *quality, struct timespec *pts); diff --git a/src/outputs/alsa.c b/src/outputs/alsa.c index 08e5d325..ede2e23d 100644 --- a/src/outputs/alsa.c +++ b/src/outputs/alsa.c @@ -967,22 +967,6 @@ alsa_device_cb_set(struct output_device *device, int callback_id) as->callback_id = callback_id; } -static void -alsa_playback_stop(void) -{ - struct alsa_session *as; - struct alsa_session *next; - - for (as = sessions; as; as = next) - { - next = as->next; - snd_pcm_drop(as->hdl); - - as->state = OUTPUT_STATE_STOPPED; - alsa_status(as); // Will stop the session - } -} - static void alsa_write(struct output_buffer *obuf) { @@ -1061,6 +1045,5 @@ struct output_definition output_alsa = .device_probe = alsa_device_probe, .device_volume_set = alsa_device_volume_set, .device_cb_set = alsa_device_cb_set, - .playback_stop = alsa_playback_stop, .write = alsa_write, }; diff --git a/src/outputs/cast.c b/src/outputs/cast.c index eabeadfc..030dca91 100644 --- a/src/outputs/cast.c +++ b/src/outputs/cast.c @@ -2062,20 +2062,6 @@ cast_device_volume_set(struct output_device *device, int callback_id) return 1; } -static void -cast_playback_stop(void) -{ - struct cast_session *cs; - struct cast_session *next; - - for (cs = cast_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_write(struct output_buffer *obuf) { @@ -2220,7 +2206,6 @@ struct output_definition output_cast = .device_flush = cast_device_flush, .device_cb_set = cast_device_cb_set, .device_volume_set = cast_device_volume_set, - .playback_stop = cast_playback_stop, .write = cast_write, .init = cast_init, .deinit = cast_deinit, diff --git a/src/outputs/dummy.c b/src/outputs/dummy.c index 8783f01b..a1865652 100644 --- a/src/outputs/dummy.c +++ b/src/outputs/dummy.c @@ -183,18 +183,6 @@ dummy_device_cb_set(struct output_device *device, int callback_id) ds->callback_id = callback_id; } -static void -dummy_playback_stop(void) -{ - struct dummy_session *ds = sessions; - - if (!sessions) - return; - - ds->state = OUTPUT_STATE_CONNECTED; - dummy_status(ds); -} - static int dummy_init(void) { @@ -245,5 +233,4 @@ struct output_definition output_dummy = .device_probe = dummy_device_probe, .device_volume_set = dummy_device_volume_set, .device_cb_set = dummy_device_cb_set, - .playback_stop = dummy_playback_stop, }; diff --git a/src/outputs/fifo.c b/src/outputs/fifo.c index f1f54e80..0e246adb 100644 --- a/src/outputs/fifo.c +++ b/src/outputs/fifo.c @@ -387,20 +387,6 @@ fifo_device_cb_set(struct output_device *device, int callback_id) fifo_session->callback_id = callback_id; } -static void -fifo_playback_stop(void) -{ - struct fifo_session *fifo_session = sessions; - - if (!fifo_session) - return; - - free_buffer(); - - fifo_session->state = OUTPUT_STATE_CONNECTED; - fifo_status(fifo_session); -} - static void fifo_write(struct output_buffer *obuf) { @@ -532,6 +518,5 @@ struct output_definition output_fifo = .device_probe = fifo_device_probe, .device_volume_set = fifo_device_volume_set, .device_cb_set = fifo_device_cb_set, - .playback_stop = fifo_playback_stop, .write = fifo_write, }; diff --git a/src/outputs/pulse.c b/src/outputs/pulse.c index ed49e809..74d2f9ac 100644 --- a/src/outputs/pulse.c +++ b/src/outputs/pulse.c @@ -771,7 +771,6 @@ pulse_device_stop(struct output_device *device, int callback_id) return 0; } - static int pulse_device_flush(struct output_device *device, int callback_id) { @@ -907,36 +906,6 @@ pulse_write(struct output_buffer *obuf) } } -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_init(void) { @@ -1032,7 +1001,6 @@ struct output_definition output_pulse = .device_free_extra = pulse_device_free_extra, .device_cb_set = pulse_device_cb_set, .device_volume_set = pulse_device_volume_set, - .playback_stop = pulse_playback_stop, .write = pulse_write, }; diff --git a/src/outputs/raop.c b/src/outputs/raop.c index 8fccb4c5..3ef3ad9e 100644 --- a/src/outputs/raop.c +++ b/src/outputs/raop.c @@ -2816,6 +2816,12 @@ raop_keep_alive_timer_cb(int fd, short what, void *arg) { struct raop_session *rs; + if (!raop_sessions) + { + event_del(keep_alive_timer); + return; + } + for (rs = raop_sessions; rs; rs = rs->next) { if (!(rs->state & RAOP_STATE_F_CONNECTED)) @@ -4850,17 +4856,6 @@ raop_device_free_extra(struct output_device *device) free(re); } -static void -raop_playback_stop(void) -{ - struct raop_session *rs; - - evtimer_del(keep_alive_timer); - - for (rs = raop_sessions; rs; rs = rs->next) - session_teardown(rs, "playback_stop"); -} - static void raop_write(struct output_buffer *obuf) { @@ -5069,7 +5064,6 @@ struct output_definition output_raop = .device_free_extra = raop_device_free_extra, .device_volume_set = raop_set_volume_one, .device_volume_to_pct = raop_volume_to_pct, - .playback_stop = raop_playback_stop, .write = raop_write, .metadata_prepare = raop_metadata_prepare, .metadata_send = raop_metadata_send, diff --git a/src/player.c b/src/player.c index 47232e44..82d25eb8 100644 --- a/src/player.c +++ b/src/player.c @@ -128,15 +128,9 @@ // When we pause, we keep the input open, but we can't do that forever. We must // think of the poor streaming servers, for instance. This timeout determines -// how long we stay paused, before we go to a full stop. +// how long we stay paused, before we close the inputs. // (value is in seconds) -#define PLAYER_PAUSE_TIME_MAX 600 - -// 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 PLAYER_STOP_TIME_MAX 10 +#define PLAYER_PAUSE_TIMEOUT 600 //#define DEBUG_PLAYER 1 @@ -291,15 +285,14 @@ static int pb_timer_fd; timer_t pb_timer; #endif static struct event *pb_timer_ev; -static struct event *player_abort_timeout_ev; +static struct event *player_pause_timeout_ev; // Time between ticks, i.e. time between when playback_cb() is invoked static struct timespec player_tick_interval; // Timer resolution static struct timespec player_timer_res; -static struct timeval player_pause_timeout = { PLAYER_PAUSE_TIME_MAX, 0 }; -static struct timeval player_stop_timeout = { PLAYER_STOP_TIME_MAX, 0 }; +static struct timeval player_pause_timeout = { PLAYER_PAUSE_TIMEOUT, 0 }; // PLAYER_WRITE_BEHIND_MAX converted to clock ticks static int pb_write_deficit_max; @@ -324,10 +317,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 ----------------------------- */ @@ -452,7 +445,7 @@ scrobble_cb(void *arg) static void pause_timer_cb(int fd, short what, void *arg) { - playback_abort(); + pb_abort(); } // Callback from the worker thread. Here the heavy lifting is done: updating the @@ -988,7 +981,7 @@ event_play_end() { DPRINTF(E_DBG, L_PLAYER, "event_play_end()\n"); - playback_abort(); + pb_abort(); } // Stuff to do when playback of current track ends @@ -1157,13 +1150,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 @@ -1229,7 +1222,7 @@ playback_cb(int fd, short what, void *arg) 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); - playback_suspend(); + pb_suspend(); } } @@ -1372,7 +1365,7 @@ device_streaming_cb(struct output_device *device, enum output_device_state statu speaker_deselect_output(device); if (output_sessions == 0) - playback_abort(); + pb_abort(); } else if (status == OUTPUT_STATE_STOPPED) { @@ -1400,7 +1393,25 @@ device_command_cb(struct output_device *device, enum output_device_state status) if (status == OUTPUT_STATE_FAILED) 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--; @@ -1408,6 +1419,8 @@ device_command_cb(struct output_device *device, enum output_device_state status) input_buffer_full_cb(player_playback_start); } + outputs_device_stop_delayed(device, device_streaming_cb); + out: commands_exec_end(cmdbase, 0); } @@ -1578,6 +1591,8 @@ player_pmap(void *p) 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 @@ -1587,14 +1602,15 @@ player_pmap(void *p) /* ------------------------- Internal playback routines --------------------- */ static int -playback_timer_start(void) +pb_timer_start(void) { struct itimerspec tick; int ret; - // The pause timer will be active if we have recently paused, but now that - // the playback loop has been kicked off, we no longer want that - event_del(player_abort_timeout_ev); + // The stop timers will be active if we have recently paused, but now that the + // playback loop has been kicked off, we deactivate them + event_del(player_pause_timeout_ev); + outputs_stop_delayed_cancel(); ret = event_add(pb_timer_ev, NULL); if (ret < 0) @@ -1623,7 +1639,7 @@ playback_timer_start(void) } static int -playback_timer_stop(void) +pb_timer_stop(void) { struct itimerspec tick; int ret; @@ -1648,7 +1664,7 @@ playback_timer_stop(void) // Initiates the session and starts the input source static int -playback_session_start(struct db_queue_item *queue_item, uint32_t seek_ms) +pb_session_start(struct db_queue_item *queue_item, uint32_t seek_ms) { struct player_source *ps; int ret; @@ -1673,45 +1689,45 @@ playback_session_start(struct db_queue_item *queue_item, uint32_t seek_ms) // The input source is now open and ready, but we might actually be paused. So // we activate the below event in case the user never starts us again - event_add(player_abort_timeout_ev, &player_pause_timeout); + event_add(player_pause_timeout_ev, &player_pause_timeout); return ret; } // Stops input source and deallocates pb_session content static void -playback_session_stop(void) +pb_session_stop(void) { source_stop(); session_stop(); - playback_timer_stop(); + pb_timer_stop(); status_update(PLAY_STOPPED); } static void -playback_abort(void) +pb_abort(void) { - outputs_playback_stop(); + // Immediate stop of all outputs + outputs_stop(device_streaming_cb); + outputs_metadata_purge(); - playback_session_stop(); + pb_session_stop(); if (!clear_queue_on_stop_disabled) db_queue_clear(0); - - outputs_metadata_purge(); } // 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); + player_flush_pending = outputs_flush(device_flush_cb); - playback_timer_stop(); + pb_timer_stop(); status_update(PLAY_PAUSED); @@ -1818,17 +1834,13 @@ playback_stop(void *arg, int *retval) // 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); - - // Stops the input - playback_session_stop(); - - status_update(PLAY_STOPPED); - + *retval = outputs_flush(device_flush_cb); outputs_metadata_purge(); - // In case we aren't restarting soon we want to make a full stop - event_add(player_abort_timeout_ev, &player_stop_timeout); + // Stops the input + pb_session_stop(); + + status_update(PLAY_STOPPED); // We're async if we need to flush devices if (*retval > 0) @@ -1837,12 +1849,21 @@ 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 = playback_timer_start(); + ret = pb_timer_start(); if (ret < 0) goto error; @@ -1852,7 +1873,7 @@ playback_start_bh(void *arg, int *retval) return COMMAND_END; error: - playback_abort(); + pb_abort(); *retval = -1; return COMMAND_END; } @@ -1914,7 +1935,7 @@ playback_start_item(void *arg, int *retval) } } - ret = playback_session_start(queue_item, seek_ms); + ret = pb_session_start(queue_item, seek_ms); if (ret < 0) { *retval = -1; @@ -2033,7 +2054,7 @@ playback_prev_bh(void *arg, int *retval) 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() + // from the output, which in streaming_cb() can cause pb_abort() if (player_state == PLAY_STOPPED) { goto error; @@ -2055,7 +2076,7 @@ playback_prev_bh(void *arg, int *retval) goto error; } - ret = playback_session_start(queue_item, 0); + ret = pb_session_start(queue_item, 0); free_queue_item(queue_item, 0); if (ret < 0) { @@ -2070,7 +2091,7 @@ playback_prev_bh(void *arg, int *retval) return COMMAND_END; error: - playback_abort(); + pb_abort(); *retval = -1; return COMMAND_END; } @@ -2083,7 +2104,7 @@ playback_next_bh(void *arg, int *retval) int id; // outputs_flush() in playback_pause() may have a caused a failure callback - // from the output, which in streaming_cb() can cause playback_abort() + // from the output, which in streaming_cb() can cause pb_abort() if (player_state == PLAY_STOPPED) { goto error; @@ -2108,7 +2129,7 @@ playback_next_bh(void *arg, int *retval) goto error; } - ret = playback_session_start(queue_item, 0); + ret = pb_session_start(queue_item, 0); free_queue_item(queue_item, 0); if (ret < 0) { @@ -2123,7 +2144,7 @@ playback_next_bh(void *arg, int *retval) return COMMAND_END; error: - playback_abort(); + pb_abort(); *retval = -1; return COMMAND_END; } @@ -2136,7 +2157,7 @@ playback_seek_bh(void *arg, int *retval) 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() + // from the output, which in streaming_cb() can cause pb_abort() if (player_state == PLAY_STOPPED) { goto error; @@ -2149,7 +2170,7 @@ playback_seek_bh(void *arg, int *retval) goto error; } - ret = playback_session_start(queue_item, cmdarg->intval); + ret = pb_session_start(queue_item, cmdarg->intval); free_queue_item(queue_item, 0); if (ret < 0) { @@ -2164,7 +2185,7 @@ playback_seek_bh(void *arg, int *retval) return COMMAND_END; error: - playback_abort(); + pb_abort(); *retval = -1; return COMMAND_END; } @@ -2176,7 +2197,7 @@ playback_pause_bh(void *arg, int *retval) 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() + // from the output, which in streaming_cb() can cause pb_abort() if (player_state == PLAY_STOPPED) { goto error; @@ -2189,7 +2210,7 @@ playback_pause_bh(void *arg, int *retval) goto error; } - ret = playback_session_start(queue_item, pb_session.playing_now->pos_ms); + ret = pb_session_start(queue_item, pb_session.playing_now->pos_ms); free_queue_item(queue_item, 0); if (ret < 0) { @@ -2205,7 +2226,7 @@ playback_pause_bh(void *arg, int *retval) return COMMAND_END; error: - playback_abort(); + pb_abort(); *retval = -1; return COMMAND_END; } @@ -2225,10 +2246,9 @@ playback_pause(void *arg, int *retval) return COMMAND_END; } - playback_timer_stop(); - - *retval = outputs_flush(device_command_cb); + pb_timer_stop(); + *retval = outputs_flush(device_flush_cb); outputs_metadata_purge(); // We're async if we need to flush devices @@ -2859,6 +2879,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) { @@ -3258,7 +3287,7 @@ player_init(void) } CHECK_NULL(L_PLAYER, evbase_player = event_base_new()); - CHECK_NULL(L_PLAYER, player_abort_timeout_ev = evtimer_new(evbase_player, pause_timer_cb, NULL)); + CHECK_NULL(L_PLAYER, player_pause_timeout_ev = evtimer_new(evbase_player, pause_timer_cb, NULL)); #ifdef HAVE_TIMERFD CHECK_NULL(L_PLAYER, pb_timer_ev = event_new(evbase_player, pb_timer_fd, EV_READ | EV_PERSIST, playback_cb, NULL)); #else @@ -3317,7 +3346,7 @@ player_deinit(void) { int ret; - player_playback_stop(); + player_playback_abort(); #ifdef HAVE_TIMERFD close(pb_timer_fd); From 992ab9087677786bdace91a27a58b7d27a8daef8 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Wed, 13 Mar 2019 21:44:16 +0100 Subject: [PATCH 62/86] [xcode] Fix missing "&" --- src/transcode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transcode.c b/src/transcode.c index 23ceb353..80bf619e 100644 --- a/src/transcode.c +++ b/src/transcode.c @@ -937,7 +937,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()); From ab0a6055b93c4611d74e1cf04f82f28139341f48 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 18 Mar 2019 22:57:18 +0100 Subject: [PATCH 63/86] [input] Refactor metadata handling + add playback wait timeout Previously input_metadata_get() would retrieve artwork from the source being read currently, which might not be the one that triggered the FLAG_METADATA event. So to fix this the metadata is now read by the input module itself when the METADATA event happens, and the result is stored with the marker. The commit also includes a timer so that the input thread does loop forever if the player never starts reading. Also some refactoring of metadata + abolish input_metadata_get and input_quality_get. The latter in an attempt to treat the two in the same way. --- src/input.c | 272 +++++++++++++++++++++++++++++----------------- src/input.h | 34 +++--- src/inputs/pipe.c | 29 +---- 3 files changed, 191 insertions(+), 144 deletions(-) diff --git a/src/input.c b/src/input.c index b418d049..0b98e336 100644 --- a/src/input.c +++ b/src/input.c @@ -44,8 +44,10 @@ // 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 in playback thread +// 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 @@ -69,9 +71,14 @@ static struct input_definition *inputs[] = { struct marker { - uint64_t pos; // Position of marker measured in bytes - struct media_quality quality; - enum input_flags flags; + // 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; @@ -116,7 +123,7 @@ 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 *inputev; +static struct event *input_ev; static bool input_initialized; // The source we are reading now @@ -128,6 +135,10 @@ static struct input_buffer input_buffer; // Timeout waiting in playback loop static struct timespec input_loop_timeout = { INPUT_LOOP_TIMEOUT, 0 }; +// Timeout waiting for player read +static struct timeval input_open_timeout = { INPUT_OPEN_TIMEOUT, 0 }; +static struct event *input_open_timeout_ev; + #ifdef DEBUG static size_t debug_elapsed; #endif @@ -160,7 +171,92 @@ map_data_kind(int data_kind) } static void -marker_add(size_t pos, short flags) +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)); +} + +static struct input_metadata * +metadata_get(struct input_source *source) +{ + struct input_metadata *metadata; + struct db_queue_item *queue_item; + int ret; + + if (!inputs[source->type]->metadata_get) + return NULL; + + metadata = calloc(1, sizeof(struct input_metadata)); + + ret = inputs[source->type]->metadata_get(metadata, source); + if (ret < 0) + goto out_free_metadata; + + 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; + } + + // 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; +} + +static void +marker_free(struct marker *marker) +{ + 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; @@ -168,8 +264,8 @@ marker_add(size_t pos, short flags) CHECK_NULL(L_PLAYER, marker = calloc(1, sizeof(struct marker))); marker->pos = pos; - marker->quality = input_buffer.cur_write_quality; - marker->flags = flags; + marker->flag = flag; + marker->data = flagdata; for (head = input_buffer.marker_tail; head && head->prev; head = head->prev) ; // Fast forward to the head @@ -180,6 +276,41 @@ marker_add(size_t pos, short flags) 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 -------------------------- */ @@ -202,9 +333,9 @@ flush(short *flags) *flags = 0; for (marker = input_buffer.marker_tail; marker; marker = input_buffer.marker_tail) { - *flags |= marker->flags; + *flags |= marker->flag; input_buffer.marker_tail = marker->prev; - free(marker); + marker_free(marker); } len = evbuffer_get_length(input_buffer.evbuf); @@ -232,7 +363,8 @@ stop(void) short flags; int type; - event_del(inputev); + event_del(input_open_timeout_ev); + event_del(input_ev); type = input_now_reading.type; @@ -339,7 +471,8 @@ start(void *arg, int *retval) 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_active(inputev, 0, 0); + 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; @@ -360,35 +493,12 @@ stop_cmd(void *arg, int *retval) return COMMAND_END; } -static enum command_state -metadata_get(void *arg, int *retval) +static void +timeout_cb(int fd, short what, void *arg) { - struct input_arg *cmdarg = arg; - int type; - - if (!input_now_reading.open) - { - DPRINTF(E_WARN, L_PLAYER, "Source is no longer available for input_metadata_get()\n"); - goto error; - } - - type = input_now_reading.type; - if ((type < 0) || (inputs[type]->disabled)) - goto error; - - if (inputs[type]->metadata_get) - *retval = inputs[type]->metadata_get(cmdarg->metadata, &input_now_reading); - else - *retval = 0; - - return COMMAND_END; - - error: - *retval = -1; - return COMMAND_END; + stop(); } - /* ---------------------- Interface towards input backends ------------------ */ /* Thread: input and spotify */ @@ -397,11 +507,14 @@ int input_write(struct evbuffer *evbuf, struct media_quality *quality, short flags) { bool read_end; + size_t len; int ret; pthread_mutex_lock(&input_buffer.mutex); 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) { @@ -423,13 +536,15 @@ input_write(struct evbuffer *evbuf, struct media_quality *quality, short flags) if (quality && !quality_is_equal(quality, &input_buffer.cur_write_quality)) { input_buffer.cur_write_quality = *quality; - marker_add(input_buffer.bytes_written, INPUT_FLAG_QUALITY); + flags |= INPUT_FLAG_QUALITY; } ret = 0; + len = 0; if (evbuf) { - input_buffer.bytes_written += evbuffer_get_length(evbuf); + len = evbuffer_get_length(evbuf); + input_buffer.bytes_written += len; ret = evbuffer_add_buffer(input_buffer.evbuf, evbuf); if (ret < 0) { @@ -440,24 +555,7 @@ input_write(struct evbuffer *evbuf, struct media_quality *quality, short flags) } if (flags) - { - if (read_end) - { - input_now_reading.open = false; - // 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); - else - // The player's read is close to our write, so open right away - marker_add(input_buffer.bytes_read, INPUT_FLAG_START_NEXT); - } - - // Note this marker is added at the post-write position, since EOF, error - // and metadata belong there. - marker_add(input_buffer.bytes_written, flags); - } + markers_set(flags, len); pthread_mutex_unlock(&input_buffer.mutex); @@ -537,7 +635,7 @@ play(evutil_socket_t fd, short flags, void *arg) int ret; // Spotify runs in its own thread, so no reading is done by the input thread, - // thus there is no reason to activate inputev + // thus there is no reason to activate input_ev if (!inputs[input_now_reading.type]->play) return; @@ -550,7 +648,7 @@ play(evutil_socket_t fd, short flags, void *arg) return; // Error or EOF, so don't come back } - event_add(inputev, &tv); + event_add(input_ev, &tv); } @@ -558,12 +656,12 @@ play(evutil_socket_t fd, short flags, void *arg) /* Thread: player */ int -input_read(void *data, size_t size, short *flags) +input_read(void *data, size_t size, short *flag, void **flagdata) { struct marker *marker; int len; - *flags = 0; + *flag = 0; pthread_mutex_lock(&input_buffer.mutex); @@ -574,9 +672,8 @@ input_read(void *data, size_t size, short *flags) marker = input_buffer.marker_tail; if (marker && marker->pos <= input_buffer.bytes_read + size) { - *flags = marker->flags; - if (*flags & INPUT_FLAG_QUALITY) - input_buffer.cur_read_quality = marker->quality; + *flag = marker->flag; + *flagdata = marker->data; size = marker->pos - input_buffer.bytes_read; input_buffer.marker_tail = marker->prev; @@ -587,7 +684,7 @@ input_read(void *data, size_t size, short *flags) if (len < 0) { DPRINTF(E_LOG, L_PLAYER, "Error reading stream data from input buffer\n"); - *flags |= INPUT_FLAG_ERROR; + *flag = INPUT_FLAG_ERROR; goto out_unlock; } @@ -595,9 +692,13 @@ input_read(void *data, size_t size, short *flags) #ifdef DEBUG // Logs if flags present or each 10 seconds + + if (*flag & INPUT_FLAG_QUALITY) + input_buffer.cur_read_quality = *((struct media_quality *)marker->data); + 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 (*flags || (debug_elapsed > 10 * one_sec_size)) + 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", @@ -611,7 +712,7 @@ input_read(void *data, size_t size, short *flags) input_buffer.cur_write_quality.channels, evbuffer_get_length(input_buffer.evbuf), input_buffer.bytes_written - input_buffer.bytes_read, - *flags); + *flag); } #endif @@ -668,37 +769,11 @@ input_flush(short *flags) flush(flags); } -int -input_quality_get(struct media_quality *quality) -{ - // No mutex, other threads should not be able to affect cur_read_quality - *quality = input_buffer.cur_read_quality; - return 0; -} - -int -input_metadata_get(struct input_metadata *metadata) -{ - struct input_arg cmdarg; - - cmdarg.metadata = metadata; - - return commands_exec_sync(cmdbase, metadata_get, NULL, &cmdarg); -} - +// 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 @@ -714,7 +789,8 @@ input_init(void) CHECK_NULL(L_PLAYER, evbase_input = event_base_new()); CHECK_NULL(L_PLAYER, input_buffer.evbuf = evbuffer_new()); - CHECK_NULL(L_PLAYER, inputev = event_new(evbase_input, -1, EV_PERSIST, play, NULL)); + 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++) @@ -761,7 +837,6 @@ input_init(void) thread_fail: commands_base_free(cmdbase); input_fail: - event_free(inputev); evbuffer_free(input_buffer.evbuf); event_base_free(evbase_input); return -1; @@ -798,7 +873,6 @@ input_deinit(void) pthread_cond_destroy(&input_buffer.cond); pthread_mutex_destroy(&input_buffer.mutex); - event_free(inputev); evbuffer_free(input_buffer.evbuf); event_base_free(evbase_input); } diff --git a/src/input.h b/src/input.h index e3797366..18407f6d 100644 --- a/src/input.h +++ b/src/input.h @@ -71,22 +71,25 @@ 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 start; - 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 @@ -164,11 +167,12 @@ input_wait(void); * * @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 @@ -212,18 +216,6 @@ input_stop(void); void input_flush(short *flags); -/* - * Returns the current quality of data returned by intput_read(). - */ -int -input_quality_get(struct media_quality *quality); - -/* - * Gets metadata from the input, returns 0 if metadata is set, otherwise -1 - */ -int -input_metadata_get(struct input_metadata *metadata); - /* * Free the entire struct */ diff --git a/src/inputs/pipe.c b/src/inputs/pipe.c index edd14357..85caaf55 100644 --- a/src/inputs/pipe.c +++ b/src/inputs/pipe.c @@ -308,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) * 1000 / pipe_sample_rate; + 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 @@ -907,27 +908,7 @@ 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) - { -// TODO this is probably broken - if (metadata->rtptime > metadata->start) - metadata->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); From 4fb45e84f243fd87254cb7d19c596720feda4cb5 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 18 Mar 2019 23:04:34 +0100 Subject: [PATCH 64/86] [player/outputs] New metadata handling (wip) --- src/outputs.c | 149 ++++++++++------------ src/outputs.h | 52 +++++--- src/outputs/raop.c | 303 +++++++++++++++------------------------------ src/player.c | 225 +++++++++------------------------ src/player.h | 4 - 5 files changed, 258 insertions(+), 475 deletions(-) diff --git a/src/outputs.c b/src/outputs.c index c084f3ec..fcfcac4c 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -37,6 +37,7 @@ #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; @@ -418,6 +419,60 @@ device_list_sort(void) 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 ---------------------------------- */ @@ -729,6 +784,12 @@ 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); @@ -899,64 +960,17 @@ outputs_write(void *buf, size_t bufsize, int nsamples, struct media_quality *qua buffer_drain(&output_buffer); } -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) +outputs_metadata_send(uint32_t item_id, bool startup, output_metadata_finalize_cb cb) { - 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); } } @@ -967,41 +981,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(); } } diff --git a/src/outputs.h b/src/outputs.h index e1b5d71e..2afc9362 100644 --- a/src/outputs.h +++ b/src/outputs.h @@ -2,6 +2,7 @@ #ifndef __OUTPUTS_H__ #define __OUTPUTS_H__ +#include #include #include #include @@ -62,6 +63,14 @@ // 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 { @@ -147,13 +156,24 @@ struct output_device struct output_device *next; }; -// Linked list of metadata prepared by each output backend struct output_metadata { enum output_types type; - void *metadata; + uint32_t item_id; - struct output_metadata *next; + // Progress data, filled out by finalize_cb() + uint32_t pos_ms; + uint32_t len_ms; + struct timespec pts; + bool startup; + + // Private output data made by the metadata_prepare() + void *priv; + + struct event *ev; + + // Finalize before right before sending, e.g. set playback position + output_metadata_finalize_cb finalize_cb; }; struct output_data @@ -172,8 +192,6 @@ struct output_buffer struct output_data data[OUTPUTS_MAX_QUALITY_SUBSCRIPTIONS + 1]; } output_buffer; -typedef void (*output_status_cb)(struct output_device *device, enum output_device_state status); - struct output_definition { // Name of the output @@ -229,11 +247,16 @@ struct output_definition // Authorize an output with a pin-code (probably coming from the filescanner) void (*authorize)(const char *pin); - // Metadata - void *(*metadata_prepare)(int id); - void (*metadata_send)(void *metadata, uint64_t rtptime, uint64_t offset, int startup); + // 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); + + // 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 @@ -317,21 +340,12 @@ outputs_stop_delayed_cancel(void); void outputs_write(void *buf, size_t bufsize, int nsamples, struct media_quality *quality, struct timespec *pts); -struct output_metadata * -outputs_metadata_prepare(int id); - void -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/raop.c b/src/outputs/raop.c index 3ef3ad9e..b9a7067d 100644 --- a/src/outputs/raop.c +++ b/src/outputs/raop.c @@ -89,7 +89,6 @@ // 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 -// TODO Should probably not be fixed, but vary with quality #define RAOP_SAMPLES_PER_PACKET 352 // How many RTP packets keep in a buffer for retransmission @@ -98,7 +97,7 @@ #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 @@ -239,15 +238,8 @@ struct raop_session struct raop_metadata { struct evbuffer *metadata; - struct evbuffer *artwork; int artwork_fmt; - - /* Progress data */ - uint32_t start; - uint32_t end; - - struct raop_metadata *next; }; struct raop_service @@ -339,8 +331,7 @@ static struct raop_service control_4svc; static struct raop_service control_6svc; /* Metadata */ -static struct raop_metadata *metadata_head; -static struct raop_metadata *metadata_tail; +static struct output_metadata *raop_cur_metadata; /* Keep-alive timer - hack for ATV's with tvOS 10 */ static struct event *keep_alive_timer; @@ -2164,138 +2155,70 @@ session_make(struct output_device *rd, int family, int callback_id, bool only_pr static void raop_metadata_free(struct raop_metadata *rmd) { - evbuffer_free(rmd->metadata); + 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) { - struct raop_metadata *rmd; + if (!raop_cur_metadata) + return; - for (rmd = metadata_head; rmd; rmd = metadata_head) - { - metadata_head = rmd->next; - - raop_metadata_free(rmd); - } - - metadata_tail = NULL; + raop_metadata_free(raop_cur_metadata->priv); + free(raop_cur_metadata); + raop_cur_metadata = 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 */ +// *** Thread: worker *** static void * -raop_metadata_prepare(int id) +raop_metadata_prepare(struct output_metadata *metadata) { 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) + queue_item = db_queue_fetch_byitemid(metadata->item_id); + if (!queue_item) { - DPRINTF(E_LOG, L_RAOP, "Out of memory for RAOP metadata\n"); - + DPRINTF(E_LOG, L_RAOP, "Could not fetch queue item\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; - } + 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 id %d; no artwork will be sent\n", id); - + 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; - 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); + 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"); - - goto out_metadata; + raop_metadata_free(rmd); + return NULL; } - /* 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; } static void @@ -2329,37 +2252,62 @@ raop_cb_metadata(struct evrtsp_request *req, void *arg) session_failure(rs); } -static int -raop_metadata_send_progress(struct raop_session *rs, struct evbuffer *evbuf, struct raop_metadata *rmd, uint32_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=%lu, 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; } - DPRINTF(E_DBG, L_PLAYER, "Metadata send is start_time=%" PRIu32 ", start=%" PRIu32 ", display=%" PRIu32 ", current=%" PRIu32 ", end=%" PRIu32 "\n", - rs->start_rtptime, rmd->start, rmd->start - delay, rmd->start + offset, rmd->end); - ret = raop_send_req_set_parameter(rs, evbuf, "text/parameters", NULL, raop_cb_metadata, "send_progress"); if (ret < 0) DPRINTF(E_LOG, L_RAOP, "Could not send SET_PARAMETER progress request to '%s'\n", rs->devname); @@ -2435,25 +2383,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, uint32_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; } @@ -2462,29 +2410,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; } @@ -2495,90 +2439,44 @@ 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; - uint32_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) - { - 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) - { - 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 = 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) { session_failure(rs); continue; } } + + // Replace current metadata with the new stuff + raop_metadata_purge(); + raop_cur_metadata = metadata; } @@ -3718,15 +3616,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); @@ -5068,7 +4964,6 @@ struct output_definition output_raop = .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/player.c b/src/player.c index 82d25eb8..d5a958d5 100644 --- a/src/player.c +++ b/src/player.c @@ -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 @@ -162,12 +146,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; @@ -184,16 +162,16 @@ union player_arg struct player_source { - /* Id of the file/item in the files database */ + // Id of the file/item in the files database uint32_t id; - /* 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 */ + // Length of the file/item in milliseconds uint32_t len_ms; - /* Quality of the source (sample rate etc.) */ + // Quality of the source (sample rate etc.) struct media_quality quality; enum data_kind data_kind; @@ -292,7 +270,7 @@ static struct timespec player_tick_interval; // Timer resolution static struct timespec player_timer_res; -static struct timeval player_pause_timeout = { PLAYER_PAUSE_TIMEOUT, 0 }; +//static struct timeval player_pause_timeout = { PLAYER_PAUSE_TIMEOUT, 0 }; // PLAYER_WRITE_BEHIND_MAX converted to clock ticks static int pb_write_deficit_max; @@ -448,87 +426,24 @@ pause_timer_cb(int fd, short what, void *arg) pb_abort(); } -// 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; - - ret = input_metadata_get(metadata); - if (ret < 0) + if (metadata->item_id != pb_session.playing_now->item_id) { - goto out_free_metadata; + 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; } - queue_item = db_queue_fetch_byitemid(metadata->item_id); - if (!queue_item) - { - DPRINTF(E_LOG, L_PLAYER, "Bug! Input metadata item_id does not match anything in queue\n"); - goto out_free_metadata; - } + 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; - // 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; - - 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; - - memset(&metadata, 0, sizeof(struct input_metadata)); - - metadata.item_id = pb_session.playing_now->item_id; - - metadata.startup = startup; - metadata.start = pb_session.playing_now->read_start; - metadata.offset = pb_session.playing_now->play_start - pb_session.playing_now->read_start; - metadata.rtptime = pb_session.pos; - - worker_execute(metadata_update_cb, &metadata, sizeof(metadata), 0); + return 0; } /* @@ -877,9 +792,10 @@ session_update_read_quality(struct media_quality *quality) int samples_per_read; if (quality_is_equal(quality, &pb_session.reading_now->quality)) - return; + goto out; 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; @@ -897,6 +813,9 @@ session_update_read_quality(struct media_quality *quality) 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 @@ -926,15 +845,11 @@ session_start(struct player_source *ps, uint32_t seek_ms) /* ------------------------- Playback event handlers ------------------------ */ static void -event_read_quality() +event_read_quality(struct media_quality *quality) { DPRINTF(E_DBG, L_PLAYER, "event_read_quality()\n"); - struct media_quality quality; - - input_quality_get(&quality); - - session_update_read_quality(&quality); + session_update_read_quality(quality); } // Stuff to do when read of current track ends @@ -969,11 +884,13 @@ event_read_start_next() } static void -event_metadata_new() +event_read_metadata(struct input_metadata *metadata) { - DPRINTF(E_DBG, L_PLAYER, "event_metadata_new()\n"); + DPRINTF(E_DBG, L_PLAYER, "event_read_metadata()\n"); - metadata_trigger(0); + outputs_metadata_send(pb_session.playing_now->item_id, false, metadata_finalize_cb); + + status_update(player_state); } static void @@ -1004,7 +921,10 @@ event_play_eof() if (consume) db_queue_delete_byitemid(pb_session.playing_now->item_id); - outputs_metadata_prune(pb_session.pos); +// outputs_metadata_prune(pb_session.pos); + + if (pb_session.reading_next) + outputs_metadata_send(pb_session.reading_next->item_id, false, metadata_finalize_cb); session_update_play_eof(); } @@ -1014,8 +934,6 @@ event_play_start() { DPRINTF(E_DBG, L_PLAYER, "event_play_start()\n"); - event_metadata_new(); - status_update(PLAY_PLAYING); session_update_play_start(); @@ -1055,7 +973,8 @@ event_read(int nsamples) static inline int source_read(int *nbytes, int *nsamples, struct media_quality *quality, uint8_t *buf, int len) { - short flags; + short flag; + void *flagdata; // Nothing to read if (!pb_session.reading_now) @@ -1080,28 +999,28 @@ source_read(int *nbytes, int *nsamples, struct media_quality *quality, uint8_t * *quality = pb_session.reading_now->quality; *nsamples = 0; - *nbytes = input_read(buf, len, &flags); - if ((*nbytes < 0) || (flags & INPUT_FLAG_ERROR)) + *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 (flags & INPUT_FLAG_START_NEXT) + else if (flag == INPUT_FLAG_START_NEXT) { event_read_start_next(); } - else if (flags & INPUT_FLAG_EOF) + else if (flag == INPUT_FLAG_EOF) { event_read_eof(); } - else if (flags & INPUT_FLAG_METADATA) + else if (flag == INPUT_FLAG_METADATA) { - event_metadata_new(); + event_read_metadata((struct input_metadata *)flagdata); } - else if (flags & INPUT_FLAG_QUALITY || quality->channels == 0) + else if (flag == INPUT_FLAG_QUALITY) { - event_read_quality(); + event_read_quality((struct media_quality *)flagdata); } if (*nbytes == 0 || quality->channels == 0) @@ -1321,25 +1240,6 @@ 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 @@ -1609,7 +1509,7 @@ pb_timer_start(void) // The stop timers will be active if we have recently paused, but now that the // playback loop has been kicked off, we deactivate them - event_del(player_pause_timeout_ev); +// event_del(player_pause_timeout_ev); outputs_stop_delayed_cancel(); ret = event_add(pb_timer_ev, NULL); @@ -1689,21 +1589,30 @@ pb_session_start(struct db_queue_item *queue_item, uint32_t seek_ms) // The input source is now open and ready, but we might actually be paused. So // we activate the below event in case the user never starts us again - event_add(player_pause_timeout_ev, &player_pause_timeout); +// event_add(player_pause_timeout_ev, &player_pause_timeout); 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(); session_stop(); - pb_timer_stop(); - status_update(PLAY_STOPPED); } @@ -1867,6 +1776,8 @@ playback_start_bh(void *arg, int *retval) if (ret < 0) goto error; + outputs_metadata_send(pb_session.playing_now->item_id, true, metadata_finalize_cb); + status_update(PLAY_PLAYING); *retval = 0; @@ -1943,8 +1854,6 @@ playback_start_item(void *arg, int *retval) } } - metadata_trigger(1); - // Start sessions on selected devices *retval = 0; @@ -2246,7 +2155,7 @@ playback_pause(void *arg, int *retval) return COMMAND_END; } - pb_timer_stop(); + pb_session_pause(); *retval = outputs_flush(device_flush_cb); outputs_metadata_purge(); @@ -3184,20 +3093,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 * diff --git a/src/player.h b/src/player.h index 70bb19bc..3d55ffcd 100644 --- a/src/player.h +++ b/src/player.h @@ -138,7 +138,6 @@ player_shuffle_set(int enable); int player_consume_set(int enable); - void player_queue_clear_history(void); @@ -157,9 +156,6 @@ 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); From 95deef9c0628d229b223d87ac0f94d7ac6edf54b Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 22 Mar 2019 22:15:13 +0100 Subject: [PATCH 65/86] A bit of scan-build fixup --- src/input.c | 4 +++- src/outputs/alsa.c | 2 +- src/outputs/raop.c | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/input.c b/src/input.c index 0b98e336..f511ceed 100644 --- a/src/input.c +++ b/src/input.c @@ -422,6 +422,8 @@ setup(struct input_source *source, struct db_queue_item *queue_item, int seek_ms if (ret < 0) goto seek_error; } + else + ret = 0; return ret; @@ -694,7 +696,7 @@ input_read(void *data, size_t size, short *flag, void **flagdata) // Logs if flags present or each 10 seconds if (*flag & INPUT_FLAG_QUALITY) - input_buffer.cur_read_quality = *((struct media_quality *)marker->data); + 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; diff --git a/src/outputs/alsa.c b/src/outputs/alsa.c index ede2e23d..10cdd91d 100644 --- a/src/outputs/alsa.c +++ b/src/outputs/alsa.c @@ -376,7 +376,7 @@ device_quality_set(struct alsa_session *as, struct media_quality *quality, char 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)); + *errmsg = safe_asprintf("Could not set hw params: %s\n", snd_strerror(ret)); goto free_params; } diff --git a/src/outputs/raop.c b/src/outputs/raop.c index b9a7067d..87991d43 100644 --- a/src/outputs/raop.c +++ b/src/outputs/raop.c @@ -1930,7 +1930,9 @@ session_teardown_cb(struct evrtsp_request *req, void *arg) rs->reqs_in_flight--; - if (!req || req->response_code != RTSP_OK) + 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; From aa3635348072db6670d004b63c1d6667006b18cb Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 22 Mar 2019 23:24:11 +0100 Subject: [PATCH 66/86] [player] Fixup crash if playback stops while sending metadata --- src/player.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/player.c b/src/player.c index d5a958d5..77e574cc 100644 --- a/src/player.c +++ b/src/player.c @@ -429,7 +429,12 @@ pause_timer_cb(int fd, short what, void *arg) static int metadata_finalize_cb(struct output_metadata *metadata) { - if (metadata->item_id != pb_session.playing_now->item_id) + if (!pb_session.playing_now) + { + 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); From 7d0ae01e84b71198295af2401f5f449cafeefa23 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 22 Mar 2019 23:25:01 +0100 Subject: [PATCH 67/86] [raop] Fixup forgotten free of pktbuffer --- src/outputs/raop.c | 2 +- src/outputs/rtp_common.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/outputs/raop.c b/src/outputs/raop.c index 87991d43..82eeba31 100644 --- a/src/outputs/raop.c +++ b/src/outputs/raop.c @@ -2294,7 +2294,7 @@ raop_metadata_rtptimes_get(uint32_t *start, uint32_t *display, uint32_t *pos, ui *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=%lu, pos_ms=%u, len_ms=%u, start=%u, display=%u, pos=%u, end=%u, rtptime=%u\n", + 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); } diff --git a/src/outputs/rtp_common.c b/src/outputs/rtp_common.c index 5a7a4074..cdb9fb80 100644 --- a/src/outputs/rtp_common.c +++ b/src/outputs/rtp_common.c @@ -113,8 +113,8 @@ rtp_session_free(struct rtp_session *session) for (i = 0; i < session->pktbuf_size; i++) free(session->pktbuf[i].data); + free(session->pktbuf); free(session->sync_packet_next.data); - free(session); } From 1b0892a53ae57416a2c25b64433000285132a2f4 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 23 Mar 2019 23:10:23 +0100 Subject: [PATCH 68/86] [player] Fix player losing quality + invalid return values from source_read() --- src/player.c | 55 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/src/player.c b/src/player.c index 77e574cc..f5bfb3c4 100644 --- a/src/player.c +++ b/src/player.c @@ -219,6 +219,10 @@ struct player_session // 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; @@ -505,7 +509,7 @@ queue_item_next(uint32_t item_id) { queue_item = db_queue_fetch_byitemid(item_id); if (!queue_item) - return NULL; + goto error; } else { @@ -517,7 +521,7 @@ queue_item_next(uint32_t item_id) queue_item = db_queue_fetch_bypos(0, shuffle); if (!queue_item) - return NULL; + goto error; } } @@ -528,6 +532,10 @@ queue_item_next(uint32_t item_id) } 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 * @@ -599,10 +607,7 @@ source_next_create(struct player_source *current) queue_item = queue_item_next(current->item_id); if (!queue_item) - { - DPRINTF(E_LOG, L_PLAYER, "Error fetching next item from queue (item-id=%d, repeat=%d)\n", current->item_id, repeat); - return NULL; - } + return NULL; ps = source_new(queue_item); @@ -796,9 +801,10 @@ session_update_read_quality(struct media_quality *quality) { int samples_per_read; - if (quality_is_equal(quality, &pb_session.reading_now->quality)) + 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; @@ -976,16 +982,25 @@ event_read(int nsamples) // Returns -1 on error or bytes read (possibly 0) static inline int -source_read(int *nbytes, int *nsamples, struct media_quality *quality, uint8_t *buf, int len) +source_read(int *nbytes, int *nsamples, uint8_t *buf, int len) { short flag; void *flagdata; - // Nothing to read + // 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 can happen if the loop tries to catch up with an overrun or a - // deficit, but the playback ends in the first iteration + // 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; @@ -993,16 +1008,11 @@ source_read(int *nbytes, int *nsamples, struct media_quality *quality, uint8_t * return 0; } - // Stream silence until event_read() stops playback + // Stream silence if playback didn't end yet memset(buf, 0, len); - *quality = pb_session.playing_now->quality; - *nbytes = len; - *nsamples = BTOS(*nbytes, quality->bits_per_sample, quality->channels); - event_read(*nsamples); return 0; } - *quality = pb_session.reading_now->quality; *nsamples = 0; *nbytes = input_read(buf, len, &flag, &flagdata); if ((*nbytes < 0) || (flag == INPUT_FLAG_ERROR)) @@ -1028,13 +1038,13 @@ source_read(int *nbytes, int *nsamples, struct media_quality *quality, uint8_t * event_read_quality((struct media_quality *)flagdata); } - if (*nbytes == 0 || quality->channels == 0) + 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, quality->bits_per_sample, quality->channels); + *nsamples = BTOS(*nbytes, pb_session.quality.bits_per_sample, pb_session.quality.channels); event_read(*nsamples); @@ -1045,7 +1055,6 @@ static void playback_cb(int fd, short what, void *arg) { struct timespec ts; - struct media_quality quality; uint64_t overrun; int nbytes; int nsamples; @@ -1104,7 +1113,7 @@ playback_cb(int fd, short what, void *arg) // should not bring us further behind, even if there is no data. for (i = 1 + overrun; i > 0; i--) { - ret = source_read(&nbytes, &nsamples, &quality, pb_session.buffer, pb_session.bufsize); + ret = source_read(&nbytes, &nsamples, pb_session.buffer, pb_session.bufsize); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Error reading from source\n"); @@ -1118,13 +1127,13 @@ playback_cb(int fd, short what, void *arg) pb_session.read_deficit -= nbytes; - outputs_write(pb_session.buffer, nbytes, nsamples, &quality, &pb_session.pts); + 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 / quality.sample_rate; + 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); From ca56ac25eda0d7d8466bf41e3eaf3cf75a221b03 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 23 Mar 2019 23:41:09 +0100 Subject: [PATCH 69/86] [input] Forgot to not timeout if player actually did read --- src/input.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/input.c b/src/input.c index f511ceed..834baabe 100644 --- a/src/input.c +++ b/src/input.c @@ -498,6 +498,11 @@ stop_cmd(void *arg, int *retval) 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(); } From 4a664cfa9fb30042329cb567598dd106c60d7f35 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 24 Mar 2019 22:53:01 +0100 Subject: [PATCH 70/86] [player] Fix playback start not showing in some clients --- src/player.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/player.c b/src/player.c index f5bfb3c4..c9ced3d9 100644 --- a/src/player.c +++ b/src/player.c @@ -723,6 +723,10 @@ 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 @@ -945,9 +949,9 @@ event_play_start() { DPRINTF(E_DBG, L_PLAYER, "event_play_start()\n"); - status_update(PLAY_PLAYING); - session_update_play_start(); + + status_update(PLAY_PLAYING); } // Checks if the new playback position requires change of play status, plus From 6930fdb28da830471f2b652aee097c632bc9591a Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 26 Mar 2019 22:08:15 +0100 Subject: [PATCH 71/86] [alsa] Fix memleak: Missing free --- src/outputs/alsa.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/outputs/alsa.c b/src/outputs/alsa.c index 10cdd91d..f00186ee 100644 --- a/src/outputs/alsa.c +++ b/src/outputs/alsa.c @@ -429,6 +429,8 @@ device_configure(struct alsa_session *as) goto out_fail; } + snd_pcm_sw_params_free(sw_params); + return 0; out_fail: From 9773d9b840a8cb8ff165340ae9f3d1072d2ab954 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 26 Mar 2019 23:13:09 +0100 Subject: [PATCH 72/86] [cast] Fallback if new mirroring app not present + metadata (not working) --- src/outputs/cast.c | 85 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 75 insertions(+), 10 deletions(-) diff --git a/src/outputs/cast.c b/src/outputs/cast.c index 030dca91..2cfc0451 100644 --- a/src/outputs/cast.c +++ b/src/outputs/cast.c @@ -70,10 +70,9 @@ #define REPLY_TIMEOUT 5 // ID of the audio mirroring app used by Chrome (Google Home) -//#define CAST_APP_ID "85CDB22F" - +#define CAST_APP_ID "85CDB22F" // Old mirroring app (Chromecast) -#define CAST_APP_ID "0F5096E8" +#define CAST_APP_ID_OLD "0F5096E8" // Namespaces #define NS_CONNECTION "urn:x-cast:com.google.cast.tp.connection" @@ -250,6 +249,8 @@ enum cast_msg_types GET_STATUS, RECEIVER_STATUS, LAUNCH, + LAUNCH_OLD, + LAUNCH_ERROR, STOP, MEDIA_CONNECT, MEDIA_CLOSE, @@ -264,6 +265,7 @@ enum cast_msg_types MEDIA_LOAD_FAILED, MEDIA_LOAD_CANCELLED, SET_VOLUME, + PRESENTATION, }; struct cast_msg_basic @@ -337,6 +339,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, @@ -418,6 +430,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, }, @@ -731,6 +749,8 @@ 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->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)) @@ -1157,6 +1177,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); @@ -1305,6 +1336,15 @@ cast_cb_flush(struct cast_session *cs, struct cast_msg_payload *payload) cast_status(cs); } +static void +cast_cb_presentation(struct cast_session *cs, struct cast_msg_payload *payload) +{ + if (!payload) + 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 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 * good then the data will be passed to cast_msg_process() that will then @@ -1492,6 +1532,15 @@ 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 ------------------ */ static struct cast_master_session * @@ -2108,6 +2157,25 @@ cast_write(struct output_buffer *obuf) } } +static void +cast_metadata_send(struct output_metadata *metadata) +{ + struct cast_session *cs; + struct cast_session *next; + + 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) { @@ -2200,6 +2268,8 @@ struct output_definition output_cast = .type = OUTPUT_TYPE_CAST, .priority = 2, .disabled = 0, + .init = cast_init, + .deinit = cast_deinit, .device_start = cast_device_start, .device_probe = cast_device_probe, .device_stop = cast_device_stop, @@ -2207,12 +2277,7 @@ struct output_definition output_cast = .device_cb_set = cast_device_cb_set, .device_volume_set = cast_device_volume_set, .write = cast_write, - .init = cast_init, - .deinit = cast_deinit, -/* TODO metadata support - .metadata_prepare = cast_metadata_prepare, +// .metadata_prepare = cast_metadata_prepare, .metadata_send = cast_metadata_send, - .metadata_purge = cast_metadata_purge, - .metadata_prune = cast_metadata_prune, -*/ +// .metadata_purge = cast_metadata_purge, }; From d10e8ab1a2bde837446eafd8d451b186d367becb Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Wed, 27 Mar 2019 22:29:28 +0100 Subject: [PATCH 73/86] [player] Fix proceed to next if track cannot be opened --- src/player.c | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/player.c b/src/player.c index c9ced3d9..e5fbaec8 100644 --- a/src/player.c +++ b/src/player.c @@ -730,11 +730,11 @@ session_update_play_start(void) } static void -session_update_read_next(void) +session_update_read_next(struct player_source *current) { struct player_source *ps; - ps = source_next_create(pb_session.reading_now); + ps = source_next_create(current); source_free(&pb_session.reading_next); pb_session.reading_next = ps; } @@ -893,7 +893,7 @@ 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(); + session_update_read_next(pb_session.reading_now); source_next(); } @@ -1585,6 +1585,7 @@ static int pb_session_start(struct db_queue_item *queue_item, uint32_t seek_ms) { struct player_source *ps; + uint32_t item_id; int ret; ps = source_new(queue_item); @@ -1595,9 +1596,10 @@ pb_session_start(struct db_queue_item *queue_item, uint32_t seek_ms) // Sets of opening of the new source while ( (ret = source_start()) < 0) { - // Couldn't start requested item, remove it from queue and try next in line - db_queue_delete_byitemid(pb_session.reading_next->item_id); - session_update_read_next(); + // 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); @@ -1605,10 +1607,6 @@ pb_session_start(struct db_queue_item *queue_item, uint32_t seek_ms) if (!pb_session.playing_now) return -1; - // The input source is now open and ready, but we might actually be paused. So - // we activate the below event in case the user never starts us again -// event_add(player_pause_timeout_ev, &player_pause_timeout); - return ret; } From 5f84fefd1ba609f24b3d5e3f1e0eb98d6af801c4 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Wed, 27 Mar 2019 22:48:09 +0100 Subject: [PATCH 74/86] [player] Remove pause timer remnants (moved to input/output in prev commit) --- src/player.c | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/player.c b/src/player.c index e5fbaec8..58d8a080 100644 --- a/src/player.c +++ b/src/player.c @@ -267,7 +267,6 @@ static int pb_timer_fd; timer_t pb_timer; #endif static struct event *pb_timer_ev; -static struct event *player_pause_timeout_ev; // Time between ticks, i.e. time between when playback_cb() is invoked static struct timespec player_tick_interval; @@ -424,12 +423,6 @@ scrobble_cb(void *arg) } #endif -static void -pause_timer_cb(int fd, short what, void *arg) -{ - pb_abort(); -} - static int metadata_finalize_cb(struct output_metadata *metadata) { @@ -1527,7 +1520,6 @@ pb_timer_start(void) // The stop timers will be active if we have recently paused, but now that the // playback loop has been kicked off, we deactivate them -// event_del(player_pause_timeout_ev); outputs_stop_delayed_cancel(); ret = event_add(pb_timer_ev, NULL); @@ -3198,7 +3190,6 @@ player_init(void) } CHECK_NULL(L_PLAYER, evbase_player = event_base_new()); - CHECK_NULL(L_PLAYER, player_pause_timeout_ev = evtimer_new(evbase_player, pause_timer_cb, NULL)); #ifdef HAVE_TIMERFD CHECK_NULL(L_PLAYER, pb_timer_ev = event_new(evbase_player, pb_timer_fd, EV_READ | EV_PERSIST, playback_cb, NULL)); #else From f1493db069abf4a88e46b5638ff8ec732082040d Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 28 Mar 2019 10:47:58 +0100 Subject: [PATCH 75/86] [player] Fix bug where device is always deselected before db_speaker_save() --- src/player.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/player.c b/src/player.c index 58d8a080..dacd8b81 100644 --- a/src/player.c +++ b/src/player.c @@ -1226,10 +1226,8 @@ device_remove_family(void *arg, int *retval) // happen?) then the device will never be removed. if (!device->session) { - if (device->selected) - speaker_deselect_output(device); - outputs_device_remove(device); + volume_master_find(); } } From 9836280029ad241617963262bd4df82c7116ab42 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 28 Mar 2019 23:06:45 +0100 Subject: [PATCH 76/86] [player] Fix issue where sync timestamps are incorrect after pb_suspend() --- src/player.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/player.c b/src/player.c index dacd8b81..9c68442f 100644 --- a/src/player.c +++ b/src/player.c @@ -826,6 +826,16 @@ session_update_read_quality(struct media_quality *quality) 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) { @@ -1635,6 +1645,13 @@ pb_abort(void) 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 @@ -1834,6 +1851,8 @@ playback_start_item(void *arg, int *retval) } 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 { From 413ce25ec6fc73fda4b6c9aed4b2deeaeda5b1dc Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 28 Mar 2019 23:22:49 +0100 Subject: [PATCH 77/86] [cast] Fixup print mask --- src/outputs/cast.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/outputs/cast.c b/src/outputs/cast.c index 2cfc0451..48def1ea 100644 --- a/src/outputs/cast.c +++ b/src/outputs/cast.c @@ -1845,7 +1845,7 @@ payload_encode(struct evbuffer *evbuf, uint8_t *rawbuf, size_t rawbuf_size, int frame = transcode_frame_new(rawbuf, rawbuf_size, nsamples, quality->sample_rate, quality->bits_per_sample); if (!frame) { - DPRINTF(E_LOG, L_CAST, "Could not convert raw PCM to frame (bufsize=%ld)\n", rawbuf_size); + DPRINTF(E_LOG, L_CAST, "Could not convert raw PCM to frame (bufsize=%zu)\n", rawbuf_size); return -1; } From 53780a7ef34122528ce3e1fd2ecdc154971eb23e Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 2 Apr 2019 22:47:11 +0200 Subject: [PATCH 78/86] [xcode] Make sample rate + channels variable This change is preparation to use ffmpeg's resampling capabilities to keep local audio in sync (by up/downsampling slightly). This requires that sample rates are not fixed for a transcode profile. Added benefit of this is that we don't need quite as many xcode profiles. --- src/artwork.c | 6 +- src/httpd.c | 8 +- src/httpd_streaming.c | 24 +++--- src/inputs/file_http.c | 2 +- src/misc.h | 4 +- src/outputs.c | 30 +++---- src/outputs/cast.c | 6 +- src/transcode.c | 175 +++++++++++++++-------------------------- src/transcode.h | 32 +++----- 9 files changed, 114 insertions(+), 173 deletions(-) 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/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_streaming.c b/src/httpd_streaming.c index c26b6dde..0e7980e8 100644 --- a/src/httpd_streaming.c +++ b/src/httpd_streaming.c @@ -47,6 +47,11 @@ extern struct event_base *evbase_httpd; // 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 { struct evhttp_request *req; @@ -140,6 +145,7 @@ streaming_end(void) 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; @@ -151,19 +157,17 @@ streaming_meta_cb(evutil_socket_t fd, short event, void *arg) goto error; decode_ctx = NULL; - if (quality.sample_rate == 44100 && quality.bits_per_sample == 16) - decode_ctx = transcode_decode_setup_raw(XCODE_PCM16_44100); - else if (quality.sample_rate == 44100 && quality.bits_per_sample == 24) - decode_ctx = transcode_decode_setup_raw(XCODE_PCM24_44100); - else if (quality.sample_rate == 48000 && quality.bits_per_sample == 16) - decode_ctx = transcode_decode_setup_raw(XCODE_PCM16_48000); - else if (quality.sample_rate == 48000 && quality.bits_per_sample == 24) - decode_ctx = transcode_decode_setup_raw(XCODE_PCM24_48000); + 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, decode_ctx, NULL, 0, 0); + streaming_encode_ctx = transcode_encode_setup(XCODE_MP3, &mp3_quality, decode_ctx, NULL, 0, 0); transcode_decode_cleanup(&decode_ctx); if (!streaming_encode_ctx) { @@ -198,7 +202,7 @@ encode_buffer(uint8_t *buffer, size_t size) samples = BTOS(size, streaming_quality.bits_per_sample, streaming_quality.channels); - frame = transcode_frame_new(buffer, size, samples, streaming_quality.sample_rate, streaming_quality.bits_per_sample); + frame = transcode_frame_new(buffer, size, samples, &streaming_quality); if (!frame) { DPRINTF(E_LOG, L_STREAMING, "Could not convert raw PCM to frame\n"); diff --git a/src/inputs/file_http.c b/src/inputs/file_http.c index 3d8a35c3..6864e103 100644 --- a/src/inputs/file_http.c +++ b/src/inputs/file_http.c @@ -34,7 +34,7 @@ setup(struct input_source *source) { struct transcode_ctx *ctx; - ctx = transcode_setup(XCODE_PCM_NATIVE, source->data_kind, source->path, source->len_ms, NULL); + ctx = transcode_setup(XCODE_PCM_NATIVE, NULL, source->data_kind, source->path, source->len_ms, NULL); if (!ctx) return -1; diff --git a/src/misc.h b/src/misc.h index 8d95b17c..220d6906 100644 --- a/src/misc.h +++ b/src/misc.h @@ -18,11 +18,11 @@ #define ARRAY_SIZE(x) ((unsigned int)(sizeof(x) / sizeof((x)[0]))) #ifndef MIN -# define MIN(a, b) ((a < b) ? a : b) +# define MIN(a, b) (((a) < (b)) ? (a) : (b)) #endif #ifndef MAX -#define MAX(a, b) ((a > b) ? a : b) +# define MAX(a, b) (((a) > (b)) ? (a) : (b)) #endif diff --git a/src/outputs.c b/src/outputs.c index fcfcac4c..1fa99109 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -250,24 +250,12 @@ device_stop_cb(struct output_device *device, enum output_device_state status) static enum transcode_profile quality_to_xcode(struct media_quality *quality) { - if (quality->sample_rate == 44100 && quality->bits_per_sample == 16) - return XCODE_PCM16_44100; - if (quality->sample_rate == 44100 && quality->bits_per_sample == 24) - return XCODE_PCM24_44100; - if (quality->sample_rate == 44100 && quality->bits_per_sample == 32) - return XCODE_PCM32_44100; - if (quality->sample_rate == 48000 && quality->bits_per_sample == 16) - return XCODE_PCM16_48000; - if (quality->sample_rate == 48000 && quality->bits_per_sample == 24) - return XCODE_PCM24_48000; - if (quality->sample_rate == 48000 && quality->bits_per_sample == 32) - return XCODE_PCM32_48000; - if (quality->sample_rate == 96000 && quality->bits_per_sample == 16) - return XCODE_PCM16_96000; - if (quality->sample_rate == 96000 && quality->bits_per_sample == 24) - return XCODE_PCM24_96000; - if (quality->sample_rate == 96000 && quality->bits_per_sample == 32) - return XCODE_PCM32_96000; + 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; } @@ -288,7 +276,7 @@ encoding_reset(struct media_quality *quality) return -1; } - decode_ctx = transcode_decode_setup_raw(profile); + 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); @@ -306,7 +294,7 @@ encoding_reset(struct media_quality *quality) profile = quality_to_xcode(&subscription->quality); if (profile != XCODE_UNKNOWN) - subscription->encode_ctx = transcode_encode_setup(profile, decode_ctx, NULL, 0, 0); + 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); @@ -354,7 +342,7 @@ buffer_fill(struct output_buffer *obuf, void *buf, size_t bufsize, struct media_ if (!output_quality_subscriptions[i].encode_ctx) continue; - frame = transcode_frame_new(buf, bufsize, nsamples, quality->sample_rate, quality->bits_per_sample); + frame = transcode_frame_new(buf, bufsize, nsamples, quality); if (!frame) continue; diff --git a/src/outputs/cast.c b/src/outputs/cast.c index 48def1ea..e9355bcf 100644 --- a/src/outputs/cast.c +++ b/src/outputs/cast.c @@ -1842,7 +1842,7 @@ payload_encode(struct evbuffer *evbuf, uint8_t *rawbuf, size_t rawbuf_size, int transcode_frame *frame; int len; - frame = transcode_frame_new(rawbuf, rawbuf_size, nsamples, quality->sample_rate, quality->bits_per_sample); + 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); @@ -2204,14 +2204,14 @@ cast_init(void) return -1; } - decode_ctx = transcode_decode_setup_raw(XCODE_PCM16_48000); + decode_ctx = transcode_decode_setup_raw(XCODE_PCM16, &cast_quality_default); if (!decode_ctx) { DPRINTF(E_LOG, L_CAST, "Could not create decoding context\n"); goto out_tls_deinit; } - cast_encode_ctx = transcode_encode_setup(XCODE_OPUS, decode_ctx, NULL, 0, 0); + 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) { diff --git a/src/transcode.c b/src/transcode.c index 80bf619e..e4170e0e 100644 --- a/src/transcode.c +++ b/src/transcode.c @@ -181,7 +181,7 @@ struct 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) { memset(settings, 0, sizeof(struct settings_ctx)); @@ -189,100 +189,29 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile) { case XCODE_PCM_NATIVE: // Sample rate and bit depth determined by source settings->encode_audio = 1; - settings->channel_layout = AV_CH_LAYOUT_STEREO; - settings->channels = 2; settings->icy = 1; break; case XCODE_PCM16_HEADER: settings->wavheader = 1; - case XCODE_PCM16_44100: + 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; break; - case XCODE_PCM16_48000: - settings->encode_audio = 1; - settings->format = "s16le"; - settings->audio_codec = AV_CODEC_ID_PCM_S16LE; - settings->sample_rate = 48000; - settings->channel_layout = AV_CH_LAYOUT_STEREO; - settings->channels = 2; - settings->sample_format = AV_SAMPLE_FMT_S16; - break; - - case XCODE_PCM16_96000: - settings->encode_audio = 1; - settings->format = "s16le"; - settings->audio_codec = AV_CODEC_ID_PCM_S16LE; - settings->sample_rate = 96000; - settings->channel_layout = AV_CH_LAYOUT_STEREO; - settings->channels = 2; - settings->sample_format = AV_SAMPLE_FMT_S16; - break; - - case XCODE_PCM24_44100: + case XCODE_PCM24: settings->encode_audio = 1; settings->format = "s24le"; settings->audio_codec = AV_CODEC_ID_PCM_S24LE; - settings->sample_rate = 44100; - settings->channel_layout = AV_CH_LAYOUT_STEREO; - settings->channels = 2; settings->sample_format = AV_SAMPLE_FMT_S32; break; - case XCODE_PCM24_48000: - settings->encode_audio = 1; - settings->format = "s24le"; - settings->audio_codec = AV_CODEC_ID_PCM_S24LE; - settings->sample_rate = 48000; - settings->channel_layout = AV_CH_LAYOUT_STEREO; - settings->channels = 2; - settings->sample_format = AV_SAMPLE_FMT_S32; - break; - - case XCODE_PCM24_96000: - settings->encode_audio = 1; - settings->format = "s24le"; - settings->audio_codec = AV_CODEC_ID_PCM_S24LE; - settings->sample_rate = 96000; - settings->channel_layout = AV_CH_LAYOUT_STEREO; - settings->channels = 2; - settings->sample_format = AV_SAMPLE_FMT_S32; - break; - - case XCODE_PCM32_44100: + case XCODE_PCM32: settings->encode_audio = 1; settings->format = "s32le"; settings->audio_codec = AV_CODEC_ID_PCM_S32LE; - settings->sample_rate = 44100; - settings->channel_layout = AV_CH_LAYOUT_STEREO; - settings->channels = 2; - settings->sample_format = AV_SAMPLE_FMT_S32; - break; - - case XCODE_PCM32_48000: - settings->encode_audio = 1; - settings->format = "s32le"; - settings->audio_codec = AV_CODEC_ID_PCM_S32LE; - settings->sample_rate = 48000; - settings->channel_layout = AV_CH_LAYOUT_STEREO; - settings->channels = 2; - settings->sample_format = AV_SAMPLE_FMT_S32; - break; - - case XCODE_PCM32_96000: - settings->encode_audio = 1; - settings->format = "s32le"; - settings->audio_codec = AV_CODEC_ID_PCM_S32LE; - settings->sample_rate = 96000; - settings->channel_layout = AV_CH_LAYOUT_STEREO; - settings->channels = 2; settings->sample_format = AV_SAMPLE_FMT_S32; break; @@ -290,9 +219,6 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile) 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; break; @@ -300,9 +226,6 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile) 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_rate = 48000; - settings->channel_layout = AV_CH_LAYOUT_STEREO; - settings->channels = 2; settings->sample_format = AV_SAMPLE_FMT_S16; // Only libopus support break; @@ -326,6 +249,23 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile) return -1; } + if (quality && quality->sample_rate) + { + settings->sample_rate = quality->sample_rate; + } + + if (quality && quality->channels) + { + 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; } @@ -352,6 +292,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) { @@ -1209,7 +1162,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; @@ -1220,7 +1173,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; @@ -1233,7 +1186,7 @@ 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; @@ -1242,17 +1195,19 @@ transcode_encode_setup(enum transcode_profile profile, struct decode_ctx *src_ct 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; - // Profile does not specify a sample rate -> use same as source + // 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; + { + ctx->settings.sample_rate = src_ctx->audio_stream.codec->sample_rate; + } - // Profile does not specify a sample format -> use same as source + // 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); @@ -1270,6 +1225,13 @@ transcode_encode_setup(enum transcode_profile profile, struct decode_ctx *src_ct } } + // 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); @@ -1297,20 +1259,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); @@ -1322,7 +1284,7 @@ transcode_setup(enum transcode_profile profile, enum data_kind data_kind, const } struct decode_ctx * -transcode_decode_setup_raw(enum transcode_profile profile) +transcode_decode_setup_raw(enum transcode_profile profile, struct media_quality *quality) { const AVCodecDescriptor *codec_desc; struct decode_ctx *ctx; @@ -1331,7 +1293,7 @@ transcode_decode_setup_raw(enum transcode_profile profile) CHECK_NULL(L_XCODE, ctx = calloc(1, sizeof(struct decode_ctx))); - if (init_settings(&ctx->settings, profile) < 0) + if (init_settings(&ctx->settings, profile, quality) < 0) { goto out_free_ctx; } @@ -1614,7 +1576,7 @@ transcode(struct evbuffer *evbuf, int *icy_timer, struct transcode_ctx *ctx, int } transcode_frame * -transcode_frame_new(void *data, size_t size, int nsamples, int sample_rate, int bits_per_sample) +transcode_frame_new(void *data, size_t size, int nsamples, struct media_quality *quality) { AVFrame *f; int ret; @@ -1626,30 +1588,19 @@ transcode_frame_new(void *data, size_t size, int nsamples, int sample_rate, int return NULL; } - if (bits_per_sample == 16) + f->format = bitdepth2format(quality->bits_per_sample); + if (f->format == AV_SAMPLE_FMT_NONE) { - f->format = AV_SAMPLE_FMT_S16; - } - else if (bits_per_sample == 24) - { - f->format = AV_SAMPLE_FMT_S32; - } - else if (bits_per_sample == 32) - { - f->format = AV_SAMPLE_FMT_S32; - } - else - { - DPRINTF(E_LOG, L_XCODE, "transcode_frame_new() called with unsupported bps (%d)\n", bits_per_sample); + 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 = sample_rate; + f->sample_rate = quality->sample_rate; f->nb_samples = nsamples; - f->channel_layout = AV_CH_LAYOUT_STEREO; + 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; @@ -1658,7 +1609,7 @@ transcode_frame_new(void *data, size_t size, int nsamples, int sample_rate, int 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, size %zu, samples %d (%d/%d/2): %s\n", size, nsamples, sample_rate, bits_per_sample, 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; } diff --git a/src/transcode.h b/src/transcode.h index 3db09ae2..6b1f753c 100644 --- a/src/transcode.h +++ b/src/transcode.h @@ -5,6 +5,7 @@ #include #include "db.h" #include "http.h" +#include "misc.h" enum transcode_profile { @@ -12,18 +13,12 @@ enum transcode_profile 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 44100 PCM16 (with wav header) + // Decodes/resamples the best audio stream into PCM16 (with wav header) XCODE_PCM16_HEADER, - // Decodes/resamples the best audio stream (no wav headers) - XCODE_PCM16_44100, - XCODE_PCM16_48000, - XCODE_PCM16_96000, - XCODE_PCM24_44100, - XCODE_PCM24_48000, - XCODE_PCM24_96000, - XCODE_PCM32_44100, - XCODE_PCM32_48000, - XCODE_PCM32_96000, + // 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 @@ -45,16 +40,16 @@ 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(enum transcode_profile profile); +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); @@ -113,14 +108,11 @@ transcode(struct evbuffer *evbuf, int *icy_timer, struct transcode_ctx *ctx, int * @in data Buffer with raw data * @in size Size of buffer * @in nsamples Number of samples in the buffer - * @in sample_rate - * Sample rate - * @in bits_per_sample - * BPS must be either 16 or 24 + * @in quality Sample rate, bits per sample and channels * @return Opaque pointer to frame if OK, otherwise NULL */ transcode_frame * -transcode_frame_new(void *data, size_t size, int nsamples, int sample_rate, int bits_per_sample); +transcode_frame_new(void *data, size_t size, int nsamples, struct media_quality *quality); void transcode_frame_free(transcode_frame *frame); From 2c778a4da08037bb87cc55f80220fb6317b5ec5a Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 4 Apr 2019 21:20:39 +0200 Subject: [PATCH 79/86] [cast] Remove check for appid, the session id check should suffice --- src/outputs/cast.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/outputs/cast.c b/src/outputs/cast.c index e9355bcf..cbc1daea 100644 --- a/src/outputs/cast.c +++ b/src/outputs/cast.c @@ -897,7 +897,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; @@ -956,11 +955,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; From 781a3c16edf5a8c662c4070d3722cfa071a45ac9 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 5 Apr 2019 23:06:05 +0200 Subject: [PATCH 80/86] [cast] Remove use of PAUSE, doesn't work with the mirroring app --- src/outputs/cast.c | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/outputs/cast.c b/src/outputs/cast.c index cbc1daea..8ba9c938 100644 --- a/src/outputs/cast.c +++ b/src/outputs/cast.c @@ -1321,7 +1321,7 @@ cast_cb_volume(struct cast_session *cs, struct cast_msg_payload *payload) cast_status(cs); } -static void +/*static void cast_cb_flush(struct cast_session *cs, struct cast_msg_payload *payload) { if (!payload) @@ -1332,7 +1332,7 @@ cast_cb_flush(struct cast_session *cs, struct cast_msg_payload *payload) cs->state = CAST_STATE_MEDIA_CONNECTED; cast_status(cs); -} +}*/ static void cast_cb_presentation(struct cast_session *cs, struct cast_msg_payload *payload) @@ -2062,17 +2062,10 @@ static int cast_device_flush(struct output_device *device, int callback_id) { struct cast_session *cs = device->session; - int ret; - - if (!(cs->state & CAST_STATE_F_MEDIA_STREAMING)) - return -1; - - // TODO Can't do this, we need to pause the stream in some other way - ret = cast_msg_send(cs, MEDIA_PAUSE, cast_cb_flush); - if (ret < 0) - return -1; cs->callback_id = callback_id; + cs->state = CAST_STATE_MEDIA_CONNECTED; + cast_status(cs); return 0; } From 02cd65a9925e9669724df527470b217836850c4e Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 8 Apr 2019 00:50:20 +0200 Subject: [PATCH 81/86] [alsa] New resample-based sync correction --- forked-daapd.conf.in | 5 -- src/conffile.c | 1 - src/misc.c | 35 ++++++++ src/misc.h | 3 + src/outputs/alsa.c | 189 ++++++++++++++++++++++++++++--------------- 5 files changed, 164 insertions(+), 69 deletions(-) diff --git a/forked-daapd.conf.in b/forked-daapd.conf.in index d85279bc..f3628811 100644 --- a/forked-daapd.conf.in +++ b/forked-daapd.conf.in @@ -223,11 +223,6 @@ audio { # ahead, positive correspond to delaying it. The unit is milliseconds. # The offset must be between -1000 and 1000 (+/- 1 sec). # offset_ms = 0 - - # 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 } # Pipe output diff --git a/src/conffile.c b/src/conffile.c index 8f0585df..19aaae92 100644 --- a/src/conffile.c +++ b/src/conffile.c @@ -117,7 +117,6 @@ static cfg_opt_t sec_audio[] = CFG_STR("mixer_device", NULL, CFGF_NONE), CFG_INT("offset", 0, CFGF_NONE), // deprecated CFG_INT("offset_ms", 0, CFGF_NONE), - CFG_INT("adjust_period_seconds", 10, CFGF_NONE), CFG_END() }; diff --git a/src/misc.c b/src/misc.c index 423d95c9..c0e283e2 100644 --- a/src/misc.c +++ b/src/misc.c @@ -1039,6 +1039,41 @@ murmur_hash64(const void *key, int len, uint32_t seed) # error Platform not supported #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) { diff --git a/src/misc.h b/src/misc.h index 220d6906..d5791148 100644 --- a/src/misc.h +++ b/src/misc.h @@ -139,6 +139,9 @@ 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); diff --git a/src/outputs/alsa.c b/src/outputs/alsa.c index f00186ee..b5952930 100644 --- a/src/outputs/alsa.c +++ b/src/outputs/alsa.c @@ -37,13 +37,28 @@ #include "player.h" #include "outputs.h" -// 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 480 +// 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 480 +// 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.2 + +// How many latency calculations we keep in the latency_history buffer +#define ALSA_LATENCY_HISTORY_SIZE 100 + +// 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) @@ -77,17 +92,23 @@ struct alsa_session uint32_t last_pos; uint32_t last_buflen; - struct timespec start_pts; struct timespec last_pts; - int last_latency; - int sync_counter; + // 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[ALSA_LATENCY_HISTORY_SIZE]; + int latency_counter; + + int sync_resample_step; // Here we buffer samples during startup struct ringbuffer prebuf; int offset_ms; - int adjust_period_seconds; int volume; long vol_min; @@ -511,7 +532,7 @@ playback_restart(struct alsa_session *as, struct output_buffer *obuf) // 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->start_pts = timespec_add(obuf->pts, ts); + 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 @@ -587,71 +608,117 @@ buffer_write(struct alsa_session *as, struct output_data *odata, snd_pcm_sframes } static enum alsa_sync_state -sync_check(struct alsa_session *as) +sync_check(double *drift, double *latency, struct alsa_session *as, snd_pcm_sframes_t delay) { enum alsa_sync_state sync; - snd_pcm_sframes_t delay; struct timespec ts; int elapsed; uint64_t cur_pos; uint64_t exp_pos; - int32_t latency; + int32_t diff; + double r2; int ret; - as->sync_counter++; - - ret = snd_pcm_delay(as->hdl, &delay); - if (ret < 0) - return ALSA_SYNC_OK; - // 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 playback was supposed to start, taking - // into account buffer time and configuration of offset_ms. We then calculate - // our expected position based on elapsed time, and if it is different from - // where we are + what is the buffers, then ALSA is out of sync. - elapsed = (ts.tv_sec - as->start_pts.tv_sec) * 1000L + (ts.tv_nsec - as->start_pts.tv_nsec) / 1000000; + // 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 - (delay + BTOS(as->prebuf.read_avail, as->quality.bits_per_sample, as->quality.channels)); + 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; - latency = cur_pos - exp_pos; + diff = cur_pos - exp_pos; - // 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) + DPRINTF(E_DBG, 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) { - as->sync_counter = 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; } - // If we have measured a consistent latency for configured period, then we take action - else if (as->sync_counter >= as->adjust_period_seconds) - { - as->sync_counter = 0; - if (latency < 0) - sync = ALSA_SYNC_BEHIND; - else - sync = ALSA_SYNC_AHEAD; - } - else - sync = ALSA_SYNC_OK; - // The will be used by sync_correct, so it knows how much we are out of sync - as->last_latency = latency; - - DPRINTF(E_DBG, L_LAUDIO, "start %lu:%lu, now %lu:%lu, elapsed is %d ms, cur_pos=%" PRIu64 ", exp_pos=%" PRIu64 ", latency=%d\n", - as->start_pts.tv_sec, as->start_pts.tv_nsec / 1000000, ts.tv_sec, ts.tv_nsec / 1000000, elapsed, cur_pos, exp_pos, latency); + 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) +sync_correct(struct alsa_session *as, double drift, double latency, struct timespec pts, snd_pcm_sframes_t delay) { - DPRINTF(E_INFO, L_LAUDIO, "Here we should take action to compensate for ALSA latency of %d samples\n", as->last_latency); - // Not implemented yet + 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 @@ -659,7 +726,10 @@ 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; @@ -689,9 +759,13 @@ playback_write(struct alsa_session *as, struct output_buffer *obuf) // Check sync each second (or if this is first write where last_pts is zero) if (obuf->pts.tv_sec != as->last_pts.tv_sec) { - sync = sync_check(as); - if (sync != ALSA_SYNC_OK) - sync_correct(as); + 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; } @@ -777,7 +851,6 @@ alsa_session_make(struct output_device *device, int callback_id) struct alsa_session *as; cfg_t *cfg_audio; char *errmsg; - int original_adjust; int ret; CHECK_NULL(L_LAUDIO, as = calloc(1, sizeof(struct alsa_session))); @@ -801,16 +874,6 @@ alsa_session_make(struct output_device *device, int callback_id) as->offset_ms = 1000 * (as->offset_ms/abs(as->offset_ms)); } - original_adjust = cfg_getint(cfg_audio, "adjust_period_seconds"); - if (original_adjust < 1) - as->adjust_period_seconds = 1; - else if (original_adjust > 20) - as->adjust_period_seconds = 20; - else - as->adjust_period_seconds = original_adjust; - if (as->adjust_period_seconds != original_adjust) - DPRINTF(E_LOG, L_LAUDIO, "Clamped ALSA adjust_period_seconds to %d\n", as->adjust_period_seconds); - snd_pcm_status_malloc(&as->pcm_status); ret = device_open(as); From b33e2665a1f1bed17ba29399e284e6f3ea7bf175 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 8 Apr 2019 21:30:29 +0200 Subject: [PATCH 82/86] [xcode] Hardcode png/jpeg pix formats (fix for #717) ffmpeg changed the behaviour of avcodec_default_get_format() so that it picks AV_PIX_FMT_MONOBLACK instead of AV_PIX_FMT_RGB24 for the png encoder. That makes the function of no use to us, so now the pix formats are just hardcoded in the settings instead. --- src/transcode.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/transcode.c b/src/transcode.c index e4170e0e..580795b8 100644 --- a/src/transcode.c +++ b/src/transcode.c @@ -234,6 +234,7 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile, str 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; @@ -241,6 +242,7 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile, str 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; From 55d9d9e6bde5dc7afbd8555e9ce760a03ba04332 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 8 Apr 2019 22:06:56 +0200 Subject: [PATCH 83/86] [xcode/artwork] Drop support for ffmpeg legacy versions --- configure.ac | 32 +- src/Makefile.am | 10 +- src/artwork_legacy.c | 1630 ---------------------------- src/ffmpeg-compat.h | 105 -- src/library/filescanner_ffmpeg.c | 10 +- src/transcode_legacy.c | 1689 ------------------------------ 6 files changed, 4 insertions(+), 3472 deletions(-) delete mode 100644 src/artwork_legacy.c delete mode 100644 src/ffmpeg-compat.h delete mode 100644 src/transcode_legacy.c 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/src/Makefile.am b/src/Makefile.am index 01ae5f64..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 \ 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/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/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/transcode_legacy.c b/src/transcode_legacy.c deleted file mode 100644 index 80421c29..00000000 --- a/src/transcode_legacy.c +++ /dev/null @@ -1,1689 +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 "misc.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_OPUS: - ctx->format = "data"; // Means we get the raw packet from the encoder, no muxing - ctx->audio_codec = AV_CODEC_ID_OPUS; - ctx->sample_rate = 48000; - ctx->channel_layout = AV_CH_LAYOUT_STEREO; - ctx->channels = 2; - ctx->sample_format = AV_SAMPLE_FMT_S16; // Only libopus support - 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(transcode_frame *frame) -{ - struct decoded_frame *decoded = frame; - - av_frame_free(&decoded->frame); - free(decoded); -} - - -/* Encoding, decoding and transcoding */ - - -int -transcode_decode(transcode_frame **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, transcode_frame *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) -{ - transcode_frame *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; -} - -transcode_frame * -transcode_frame_new(enum transcode_profile profile, void *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 = BTOS(size); - 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, 1); - 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; -} - From 40934e71625f3b3e958280e59be1ea427eb7da1f Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 9 Apr 2019 21:03:14 +0200 Subject: [PATCH 84/86] [cast] Remove/disable some unused code Leaving some of the MEDIA commands in, even though they are not used any more. Might come in handy later. --- src/outputs/cast.c | 80 ++++------------------------------------------ 1 file changed, 7 insertions(+), 73 deletions(-) diff --git a/src/outputs/cast.c b/src/outputs/cast.c index 8ba9c938..f646fc05 100644 --- a/src/outputs/cast.c +++ b/src/outputs/cast.c @@ -152,17 +152,6 @@ enum cast_state CAST_STATE_MEDIA_CONNECTED = CAST_STATE_F_MEDIA_CONNECTED, // After OFFER CAST_STATE_MEDIA_STREAMING = CAST_STATE_F_MEDIA_CONNECTED | CAST_STATE_F_MEDIA_STREAMING, - -/* - // 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_STREAMING, - // After PLAY - CAST_STATE_MEDIA_PLAYING = CAST_STATE_F_MEDIA_CONNECTED | CAST_STATE_F_MEDIA_LOADED | CAST_STATE_F_MEDIA_STREAMING | 0x01, -*/ }; struct cast_master_session @@ -1019,13 +1008,6 @@ 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_STREAMING: - state = OUTPUT_STATE_STREAMING; - break; -*/ case CAST_STATE_MEDIA_STREAMING: state = OUTPUT_STATE_STREAMING; break; @@ -1276,64 +1258,13 @@ 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 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; - } -// TODO check result == "ok" - else if (!payload->udp_port) - { - DPRINTF(E_LOG, L_CAST, "Missing UDP port in ANSWER - aborting\n"); - goto error; - } - - DPRINTF(E_LOG, 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; - - cs->state = CAST_STATE_MEDIA_LOADED; - - 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) -{ - if (!payload) - DPRINTF(E_LOG, L_CAST, "No reply to PAUSE 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_CONNECTED; - - cast_status(cs); -}*/ - +/* static void cast_cb_presentation(struct cast_session *cs, struct cast_msg_payload *payload) { @@ -1342,6 +1273,7 @@ cast_cb_presentation(struct cast_session *cs, struct cast_msg_payload *payload) else if (payload->type != MEDIA_STATUS) 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 @@ -1532,12 +1464,13 @@ 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 ------------------ */ @@ -1740,7 +1673,6 @@ 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_STREAMING: case CAST_STATE_MEDIA_STREAMING: ret = cast_msg_send(cs, MEDIA_STOP, cast_cb_stop_media); pending = 1; @@ -2148,6 +2080,7 @@ cast_write(struct output_buffer *obuf) } } +/* Doesn't work, but left here so it can be fixed static void cast_metadata_send(struct output_metadata *metadata) { @@ -2166,6 +2099,7 @@ cast_metadata_send(struct output_metadata *metadata) // TODO free the metadata } +*/ static int cast_init(void) @@ -2269,6 +2203,6 @@ struct output_definition output_cast = .device_volume_set = cast_device_volume_set, .write = cast_write, // .metadata_prepare = cast_metadata_prepare, - .metadata_send = cast_metadata_send, +// .metadata_send = cast_metadata_send, // .metadata_purge = cast_metadata_purge, }; From dc65cb5b762f34c8d0292dffcedd0c6a32aa9e64 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 9 Apr 2019 21:45:16 +0200 Subject: [PATCH 85/86] [alsa] Make sync and sync evaluation period configurable --- forked-daapd.conf.in | 21 ++++++++++++++++----- src/conffile.c | 2 ++ src/outputs/alsa.c | 25 +++++++++++++++---------- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/forked-daapd.conf.in b/forked-daapd.conf.in index f3628811..ecdeacbe 100644 --- a/forked-daapd.conf.in +++ b/forked-daapd.conf.in @@ -217,12 +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 other speakers, e.g. Airplay, - # adjust this value. 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). + # 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 + + # 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/conffile.c b/src/conffile.c index 19aaae92..42a26040 100644 --- a/src/conffile.c +++ b/src/conffile.c @@ -115,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_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/outputs/alsa.c b/src/outputs/alsa.c index b5952930..831603d5 100644 --- a/src/outputs/alsa.c +++ b/src/outputs/alsa.c @@ -49,9 +49,6 @@ // if r2 is below this value we won't attempt to correct sync. #define ALSA_MAX_VARIANCE 0.2 -// How many latency calculations we keep in the latency_history buffer -#define ALSA_LATENCY_HISTORY_SIZE 100 - // 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 @@ -100,7 +97,7 @@ struct alsa_session // Array of latency calculations, where latency_counter tells how many are // currently in the array - double latency_history[ALSA_LATENCY_HISTORY_SIZE]; + double *latency_history; int latency_counter; int sync_resample_step; @@ -123,6 +120,9 @@ struct alsa_session static struct alsa_session *sessions; +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 }; @@ -636,20 +636,20 @@ sync_check(double *drift, double *latency, struct alsa_session *as, snd_pcm_sfra exp_pos = (uint64_t)elapsed * as->quality.sample_rate / 1000; diff = cur_pos - exp_pos; - DPRINTF(E_DBG, 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); + 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) + 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); + 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"); @@ -657,7 +657,7 @@ sync_check(double *drift, double *latency, struct alsa_session *as, snd_pcm_sfra } // Set *latency to the "average" within the period - *latency = (*drift) * ALSA_LATENCY_HISTORY_SIZE / 2 + (*latency); + *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 @@ -757,7 +757,7 @@ playback_write(struct alsa_session *as, struct output_buffer *obuf) } // Check sync each second (or if this is first write where last_pts is zero) - if (obuf->pts.tv_sec != as->last_pts.tv_sec) + if (!alsa_sync_disable && (obuf->pts.tv_sec != as->last_pts.tv_sec)) { ret = snd_pcm_delay(as->hdl, &delay); if (ret == 0) @@ -874,6 +874,8 @@ alsa_session_make(struct output_device *device, int callback_id) 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); @@ -1065,6 +1067,9 @@ alsa_init(void) if (type && (strcasecmp(type, "alsa") != 0)) return -1; + alsa_sync_disable = cfg_getbool(cfg_audio, "sync_disable"); + alsa_latency_history_size = cfg_getint(cfg_audio, "adjust_period_seconds"); + CHECK_NULL(L_LAUDIO, device = calloc(1, sizeof(struct output_device))); device->id = 0; From ce2be1a724428622283a922ba8b2da32190f2c3f Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Wed, 10 Apr 2019 22:38:48 +0200 Subject: [PATCH 86/86] [-] A bit of cleaning up --- src/input.c | 8 ++++---- src/outputs/alsa.c | 2 +- src/player.c | 10 ---------- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/input.c b/src/input.c index 834baabe..aa062318 100644 --- a/src/input.c +++ b/src/input.c @@ -49,7 +49,7 @@ // 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; @@ -139,7 +139,7 @@ static struct timespec input_loop_timeout = { INPUT_LOOP_TIMEOUT, 0 }; static struct timeval input_open_timeout = { INPUT_OPEN_TIMEOUT, 0 }; static struct event *input_open_timeout_ev; -#ifdef DEBUG +#ifdef DEBUG_INPUT static size_t debug_elapsed; #endif @@ -352,7 +352,7 @@ flush(short *flags) pthread_mutex_unlock(&input_buffer.mutex); -#ifdef DEBUG +#ifdef DEBUG_INPUT DPRINTF(E_DBG, L_PLAYER, "Flushing %zu bytes with flags %d\n", len, *flags); #endif } @@ -697,7 +697,7 @@ input_read(void *data, size_t size, short *flag, void **flagdata) input_buffer.bytes_read += len; -#ifdef DEBUG +#ifdef DEBUG_INPUT // Logs if flags present or each 10 seconds if (*flag & INPUT_FLAG_QUALITY) diff --git a/src/outputs/alsa.c b/src/outputs/alsa.c index 831603d5..d076b74d 100644 --- a/src/outputs/alsa.c +++ b/src/outputs/alsa.c @@ -47,7 +47,7 @@ // If latency is jumping up and down we don't do compensation since we probably // 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.2 +#define ALSA_MAX_VARIANCE 0.3 // We correct latency by adjusting the sample rate in steps. However, if the // latency keeps drifting we give up after reaching this step. diff --git a/src/player.c b/src/player.c index 9c68442f..1054b189 100644 --- a/src/player.c +++ b/src/player.c @@ -110,12 +110,6 @@ // (value is in milliseconds) #define PLAYER_WRITE_BEHIND_MAX 1500 -// When we pause, we keep the input open, but we can't do that forever. We must -// think of the poor streaming servers, for instance. This timeout determines -// how long we stay paused, before we close the inputs. -// (value is in seconds) -#define PLAYER_PAUSE_TIMEOUT 600 - //#define DEBUG_PLAYER 1 struct volume_param { @@ -273,8 +267,6 @@ static struct timespec player_tick_interval; // Timer resolution static struct timespec player_timer_res; -//static struct timeval player_pause_timeout = { PLAYER_PAUSE_TIMEOUT, 0 }; - // PLAYER_WRITE_BEHIND_MAX converted to clock ticks static int pb_write_deficit_max; @@ -939,8 +931,6 @@ event_play_eof() if (consume) db_queue_delete_byitemid(pb_session.playing_now->item_id); -// outputs_metadata_prune(pb_session.pos); - if (pb_session.reading_next) outputs_metadata_send(pb_session.reading_next->item_id, false, metadata_finalize_cb);