mirror of
https://github.com/owntone/owntone-server.git
synced 2024-12-28 08:05:56 -05:00
[player] Use input progress metadata to update clients
Before, we were ignoring progress metadata, and we were also updating clients and db too early with input metadata (right when read from the input, instead of waiting until playback (speakers) were at that position. This change adds a complicated async chain of events from when the update is received.
This commit is contained in:
parent
68022d5c10
commit
e570cbdcbd
37
src/input.c
37
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)
|
||||
{
|
||||
|
14
src/input.h
14
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);
|
||||
|
280
src/player.c
280
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");
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user