diff --git a/src/input.c b/src/input.c index c43cdcbc..307616ca 100644 --- a/src/input.c +++ b/src/input.c @@ -177,6 +177,9 @@ map_data_kind(int data_kind) static void metadata_free(struct input_metadata *metadata, int content_only) { + if (!metadata) + return; + free(metadata->artist); free(metadata->title); free(metadata->album); @@ -193,7 +196,6 @@ 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) @@ -205,37 +207,7 @@ metadata_get(struct input_source *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); + metadata->item_id = source->item_id; return metadata; @@ -867,7 +839,6 @@ input_flush(short *flags) flush(flags); } -// Not currently used, perhaps remove? void input_metadata_free(struct input_metadata *metadata, int content_only) { diff --git a/src/input.h b/src/input.h index 0e0c6afa..d426b5c9 100644 --- a/src/input.h +++ b/src/input.h @@ -74,9 +74,12 @@ struct input_metadata // queue_item id uint32_t item_id; - // Input can override the default player progress by setting this - // FIXME only implemented for Airplay speakers currently - uint32_t pos_ms; + // Input can override the default player progress by setting this. For the + // other fields the receiver can check whether an update happened by checking + // if it is non-zero/null, but not for pos_ms since 0 and even negative values + // are valid. + bool pos_is_updated; + int32_t pos_ms; // Sets new song length (input will also update queue_item) uint32_t len_ms; @@ -87,9 +90,6 @@ struct input_metadata char *album; char *genre; char *artwork_url; - - // Indicates whether we are starting playback. Just passed on to output. - int startup; }; struct input_definition @@ -219,7 +219,7 @@ void input_flush(short *flags); /* - * Free the entire struct + * Free input_metadata */ void input_metadata_free(struct input_metadata *metadata, int content_only); diff --git a/src/player.c b/src/player.c index 8e04fd5e..6d9ce000 100644 --- a/src/player.c +++ b/src/player.c @@ -175,9 +175,14 @@ struct player_source // 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, 0 for endless (unless the input + // has given us a track length) uint32_t len_ms; + // Set when opening the item based on initial track length (so is not changed + // by later input track length metadata) + bool is_seekable; + // Quality of the source (sample rate etc.) struct media_quality quality; @@ -199,8 +204,14 @@ struct player_source uint64_t play_start; uint64_t play_end; + // When we receive a metadata update from the input it shouldn't be pushed to + // clients until the speakers have reached the position that matches the + // position the player was reading at when it got INPUT_FLAG_METADATA. + uint64_t metadata_update; + // 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; @@ -308,6 +319,14 @@ static uint32_t cur_plversion; // Play history static struct player_history *history; +// When we receive track metadata from the input we have to wait until playback +// has reached the position before using it. We use this to record the update. +struct metadata_pending_register +{ + uint64_t pos; + struct input_metadata *metadata; +} metadata_pending[16]; + /* -------------------------------- Forwards -------------------------------- */ @@ -348,31 +367,6 @@ scrobble_cb(void *arg) } #endif -static int -metadata_finalize_cb(struct output_metadata *metadata) -{ - 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); - return -1; - } - - 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; - - return 0; -} - /* * Add the song with the given id to the list of previously played songs */ @@ -470,6 +464,132 @@ status_update(enum play_status status) listener_notify(LISTENER_PLAYER); } +/* ------ All this is for dealing with metadata received from the input ----- */ + +static int +metadata_pending_add(struct input_metadata *metadata, uint64_t pos) +{ + int i; + + if (pos == 0) + return -1; // Invalid position + + for (i = 0; i < ARRAY_SIZE(metadata_pending); i++) + { + if (metadata_pending[i].metadata == NULL) + break; + } + + if (i == ARRAY_SIZE(metadata_pending)) + { + DPRINTF(E_LOG, L_PLAYER, "Error, too many pending metadata updates\n"); + return -1; + } + + metadata_pending[i].pos = pos; + metadata_pending[i].metadata = metadata; + return 0; +} + +static uint64_t +metadata_pending_next_pos(void) +{ + uint64_t next_pos; + int i; + + for (i = 0, next_pos = 0; i < ARRAY_SIZE(metadata_pending); i++) + { + if (metadata_pending[i].metadata && (metadata_pending[i].pos < next_pos || !next_pos)) + next_pos = metadata_pending[i].pos; + } + + return next_pos; +} + +static int +metadata_finalize_cb(struct output_metadata *metadata) +{ + 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); + return -1; + } + + 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; + + return 0; +} + +static enum command_state +metadata_finalize(void *arg, int *retval) +{ + if (!pb_session.playing_now) + return COMMAND_END; // Playback ended while we doing the metadata update + + outputs_metadata_send(pb_session.playing_now->item_id, false, metadata_finalize_cb); + + status_update(player_state); + + return COMMAND_END; +} + +// Done in worker thread because we avoid blocking db updates in the player +static void +metadata_update_queue_cb(void *arg) +{ + struct input_metadata *metadata = *(struct input_metadata **)arg; + struct db_queue_item *queue_item; + int ret; + + queue_item = db_queue_fetch_byitemid(metadata->item_id); + if (!queue_item) + { + DPRINTF(E_LOG, L_PLAYER, "Bug! Could not update queue metadata, the item_id is unknown (%u)\n", metadata->item_id); + input_metadata_free(metadata, 0); + return; + } + + // 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); + input_metadata_free(metadata, 0); + + // Now return to the player thread and run metadata_finalize + commands_exec_async(cmdbase, metadata_finalize, NULL); +} + /* ----------- Audio source handling (interfaces with input module) --------- */ @@ -500,6 +620,7 @@ source_create(struct db_queue_item *queue_item, uint32_t seek_ms) ps->data_kind = queue_item->data_kind; ps->media_kind = queue_item->media_kind; ps->len_ms = queue_item->song_length; + ps->is_seekable = (queue_item->song_length > 0); ps->path = strdup(queue_item->path); ps->seek_ms = seek_ms; @@ -595,8 +716,9 @@ source_print(char *line, size_t linesize, struct player_source *ps, const char * pos += snprintf(line + pos, linesize - pos, "%s.play_start=%" PRIu64 "; ", name, ps->play_start); pos += snprintf(line + pos, linesize - pos, "%s.read_end=%" PRIu64 "; ", name, ps->read_end); pos += snprintf(line + pos, linesize - pos, "%s.play_end=%" PRIu64 "; ", 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); + pos += snprintf(line + pos, linesize - pos, "%s.metadata_update=%" PRIu64 "; ", name, ps->metadata_update); + pos += snprintf(line + pos, linesize - pos, "%s.pos_ms=%u; ", name, ps->pos_ms); + pos += snprintf(line + pos, linesize - pos, "%s.seek_ms=%u; ", name, ps->seek_ms); } else pos += snprintf(line + pos, linesize - pos, "%s=(null); ", name); @@ -712,6 +834,8 @@ session_update_read_start(uint32_t seek_ms) static inline void session_update_read(int nsamples) { + uint32_t step_ms; + // Did we just complete our first read? Then set the start timestamp if (pb_session.start_ts.tv_sec == 0) { @@ -722,9 +846,15 @@ session_update_read(int nsamples) // Advance position pb_session.pos += nsamples; + // Need to know sample rate to calculate pos_ms step + if (!pb_session.playing_now->quality.sample_rate) + return; + + step_ms = (1000 * nsamples) / pb_session.playing_now->quality.sample_rate; + // 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; + if (pb_session.pos > pb_session.playing_now->play_start) + pb_session.playing_now->pos_ms += step_ms; } static void @@ -764,6 +894,25 @@ session_update_read_quality(struct media_quality *quality) free(quality); } +static void +session_update_read_metadata(void) +{ + if (!pb_session.reading_now) + return; + + // Sets when to trigger the next event_play_metadata() + pb_session.reading_now->metadata_update = metadata_pending_next_pos(); +} + +static void +session_update_play_metadata(struct input_metadata *metadata) +{ + if (metadata->pos_is_updated) + pb_session.playing_now->pos_ms = metadata->pos_ms; + if (metadata->len_ms) + pb_session.playing_now->len_ms = metadata->len_ms; +} + static void session_restart(void) { @@ -850,16 +999,31 @@ event_read_start_next() static void event_read_metadata(struct input_metadata *metadata) { + uint64_t delay; + int ret; + DPRINTF(E_DBG, L_PLAYER, "event_read_metadata()\n"); - // FIXME Right now, metadata->pos_ms is not honoured. If it is >0 it is sent - // to the outputs, but the player's pos_ms is not adjusted. That means we - // don't always show correct progress for http streams, pipes and files with - // chapters. + // Add the metadata to the register of pending events with a trigger position + // that corresponds to OUTPUTS_BUFFER_DURATION into the future. If we have + // received a negative position we assume the metadata needs to be delayed + // until the position is 0. + if (metadata->pos_is_updated && metadata->pos_ms < 0) + { + delay = pb_session.reading_now->output_buffer_samples + (-metadata->pos_ms) * (uint64_t)pb_session.reading_now->quality.sample_rate / 1000; + metadata->pos_ms = 0; + } + else + delay = pb_session.reading_now->output_buffer_samples; - outputs_metadata_send(pb_session.playing_now->item_id, false, metadata_finalize_cb); + ret = metadata_pending_add(metadata, pb_session.pos + delay); + if (ret < 0) + { + input_metadata_free(metadata, 0); + return; + } - status_update(player_state); + session_update_read_metadata(); } static void @@ -912,6 +1076,38 @@ event_play_start() status_update(PLAY_PLAYING); } +static void +event_play_metadata() +{ + int i; + + DPRINTF(E_DBG, L_PLAYER, "event_play_metadata()\n"); + + for (i = 0; i < ARRAY_SIZE(metadata_pending); i++) + { + // Proces all events with position from metadata_update (included) to + // current read position (excluded) + if (!(metadata_pending[i].pos >= pb_session.playing_now->metadata_update && metadata_pending[i].pos < pb_session.pos)) + continue; + + // Just in case + if (!metadata_pending[i].metadata) + continue; + + session_update_play_metadata(metadata_pending[i].metadata); + + // Triggers an async chain of metadata update, first worker will do an + // update of the db, then the player will update outputs, where the worker + // may be called by the output, and then player sends status_update + worker_execute(metadata_update_queue_cb, &(metadata_pending[i].metadata), sizeof(metadata_pending[i].metadata), 0); + + memset(&metadata_pending[i], 0, sizeof(struct metadata_pending_register)); + } + + // Set trigger (playing_now->metadata_update) to next pending metadata + session_update_read_metadata(); +} + // Checks if the new playback position requires change of play status, plus // calls session_update_read that updates playback position static inline void @@ -935,8 +1131,16 @@ event_read(int nsamples) session_update_read(nsamples); // Check if the playback position passed the play_start position - if (pb_session.pos - nsamples < pb_session.playing_now->play_start && pb_session.pos >= pb_session.playing_now->play_start) + if (pb_session.pos > pb_session.playing_now->play_start && pb_session.pos <= pb_session.playing_now->play_start + nsamples) event_play_start(); + + if (pb_session.playing_now->metadata_update == 0) + return; + + // Check if the playback position passed an input metadata update. The event + // must process all metadata updates in the read interval. + if (pb_session.pos > pb_session.playing_now->metadata_update && pb_session.pos <= pb_session.playing_now->metadata_update + nsamples) + event_play_metadata(); } @@ -2178,7 +2382,7 @@ static enum command_state playback_seek(void *arg, int *retval) { // Only check if the current playing track is seekable, other checks will be done in playback_pause() - if (pb_session.playing_now && pb_session.playing_now->len_ms <= 0) + if (pb_session.playing_now && !pb_session.playing_now->is_seekable) { DPRINTF(E_WARN, L_PLAYER, "Failed to seek, track is not seekable\n");