From be0509535fa526f787cd8c5267cadf13b92eead7 Mon Sep 17 00:00:00 2001 From: Not Zed Date: Tue, 9 Jan 2024 21:07:09 +1030 Subject: [PATCH] Completely re-did player state to use per-playlist state. --- README | 8 + dbindex.c | 53 +++++- dbindex.h | 37 ++-- http-monitor.c | 194 ++++++++++++++++--- index-util.c | 121 ++++++++++++ music-player.c | 500 ++++++++++++++++++++++--------------------------- player.html | 6 +- 7 files changed, 584 insertions(+), 335 deletions(-) diff --git a/README b/README index f5de658..7f7ad0a 100644 --- a/README +++ b/README @@ -72,6 +72,14 @@ System playlists * user list A user playlist +Playing State + +The play state is stored for each list. There should be only one (if +any) with the 'state' value non-zero. + +ISSUE: How to resolve a user-selected playlist if that is active since + everything else is pre-known. + Operations on Search * Play Now diff --git a/dbindex.c b/dbindex.c index c282f6d..a390127 100644 --- a/dbindex.c +++ b/dbindex.c @@ -77,6 +77,8 @@ struct dblistfile { struct dbindex { int res; // last result + int debug; + MDB_env *env; MDB_dbi meta; @@ -211,8 +213,8 @@ dbindex *dbindex_open(const char *ipath) { // setup system playlists { - static const char *names[] = { "all", "all#shuffle", "jukebox", "jukebox#shuffle", "playnow", "playnow#shuffle", "shit" }; - static const char *descs[] = { "All files ordered by path", "All files shuffled", "All enqueued songs in order", "All endqueued songs shuffled", "All play-now files in order", "All play-now files shuffled", "The shitlist" }; + static const char *names[] = { "all", "all#shuffle", "jukebox", "jukebox#shuffle", "playnow", "playnow#shuffle", "history", "shit" }; + static const char *descs[] = { "All files ordered by path", "All files shuffled", "All enqueued songs in order", "All endqueued songs shuffled", "All play-now files in order", "All play-now files shuffled", "History", "The shitlist" }; dblist list = { 0 }; for (int i=0;ires = mdb_get(tx, db->meta, &key, &data); if (db->res == 0) { dbstate *p = (dbstate *)data.mv_data; - if (p->size != sizeof(dbstate)) + if (p->version != ZPL_STATE_VERSION) return MDB_NOTFOUND; + + if (db->debug > 1) { + printf("dbstate get\n"); + printf(" state: %08x\n", p->state); + printf(" listd: %d\n", p->curr.listid); + printf(" seq: %d\n", p->curr.seq); + printf(" filed: %d\n", p->curr.fileid); + } + *s = *p; return 0; } else { @@ -308,11 +324,27 @@ int dbstate_get(MDB_txn *tx, dbindex *db, dbstate *s) { return db->res; } -int dbstate_put(MDB_txn *tx, dbindex *db, dbstate *s) { - MDB_val key = { .mv_data = "state", .mv_size = strlen("state") }; +int dbstate_del_id(MDB_txn *tx, dbindex *db, dbid_t listid) { + MDB_val key; + char id[32]; + + sprintf(id, "state#%016x", listid); + key.mv_data = id; + key.mv_size = strlen(id); + + return mdb_del(tx, db->meta, &key, NULL); +} + +int dbstate_put(MDB_txn *tx, dbindex *db, dbid_t listid, dbstate *s) { + MDB_val key; MDB_val data = { .mv_data = s, .mv_size = sizeof(*s) }; + char id[32]; + + sprintf(id, "state#%016x", listid); + key.mv_data = id; + key.mv_size = strlen(id); - s->size = sizeof(dbstate); + s->version = ZPL_STATE_VERSION; return mdb_put(tx, db->meta, &key, &data, 0); } @@ -1335,6 +1367,9 @@ int dblist_del(dbtxn *txn, dbindex *db, int listid) { if ((res = mdb_del(tx, db->file_by_list, &key, NULL))) goto fail; + if ((res = dbstate_del_id(tx, db, listid))) + goto fail; + res = mdb_cursor_open(tx, db->list_by_file, &cursor); printf("delete reverse list\n"); @@ -1749,6 +1784,7 @@ int dbfile_searchx(dbindex *db, const char *pattern, dbfile **results, int maxle } else break; } + mdb_cursor_close(cursor); regfree(®); mdb_txn_abort(tx); @@ -1808,6 +1844,7 @@ int dbfile_search(dbindex *db, const char *pattern, dbfile **results, int maxlen } } + mdb_cursor_close(cursor); regfree(®); mdb_txn_abort(tx); diff --git a/dbindex.h b/dbindex.h index fc74aba..8ea61de 100644 --- a/dbindex.h +++ b/dbindex.h @@ -72,36 +72,32 @@ struct dblistcursor { dbid_t fileid; }; -/* player state, this is passed around as a struct */ +/* player state, this isn't blobbed */ typedef struct dbstate dbstate; // current list mode -// or just list mode to try on mode finish (doesn't need to be stored?) enum list_mode_t { - LIST_DEFAULT, - LIST_JUKEBOX, - LIST_USER, - LIST_PLAYNOW + ZPL_DEFAULT, + ZPL_USER, + ZPL_JUKEBOX, + ZPL_PLAYNOW, + ZPL_SIZEOF }; struct dbstate { - uint32_t size; // don't need to initialise - uint32_t state; // some info on playing - - uint32_t list_mode; // which list are we playing - - dblistcursor current; // current file for restore - - dblistcursor list_playnow;// the playnow list (records history) - dblistcursor list_user; // user requested list/position - dblistcursor list_jukebox; // jukebox list position - dblistcursor list_default; // default list position + uint32_t version; // version written, matches source + uint32_t state; // some info on playing + time_t stamp; // last update time uint64_t pos; // last approximate position pts double poss; // last approximate position in seconds - time_t stamp; // last update time + + dblistcursor curr; // current track/list info }; +// current version/magic +#define ZPL_STATE_VERSION 0x5a5e0001 + typedef struct dbindex dbindex; typedef struct MDB_txn dbtxn; typedef struct dbscan dbscan; @@ -124,8 +120,9 @@ dbtxn *dbindex_begin(dbindex *db, dbtxn *txn, int readonly); int dbindex_commit(dbtxn *tx); void dbindex_abort(dbtxn *tx); -int dbstate_get(dbtxn *tx, dbindex *db, dbstate *s); -int dbstate_put(dbtxn *tx, dbindex *db, dbstate *s); +int dbstate_get(dbtxn *tx, dbindex *db, dbid_t listid, dbstate *s); +int dbstate_put(dbtxn *tx, dbindex *db, dbid_t listid, dbstate *s); +int dbstate_del_id(MDB_txn *tx, dbindex *db, dbid_t listid); //int dbstate_get_list(dbtxn *tx, dbindex *db, dbid_t listid, dblistcursor *c); //int dbstate_put_list(dbtxn *tx, dbindex *db, dbid_t listid, dblistcursor *c); diff --git a/http-monitor.c b/http-monitor.c index e8460ec..6fc480c 100644 --- a/http-monitor.c +++ b/http-monitor.c @@ -87,11 +87,13 @@ static int blobio_load(struct ez_blobio *io, const char *path, struct obstack *o return res; } +#if 0 static void dump_params(struct ez_httprequest *r) { printf("request: '%s'\n", r->url); for (struct ez_pair *w = ez_list_head(&r->params), *n = ez_node_succ(w);n;w=n,n = ez_node_succ(w)) printf(" %s=%s\n", w->name, w->value); } +#endif static struct ez_pair *find_param(struct ez_httprequest *r, const char *name) { for (struct ez_pair *w = ez_list_head(&r->params), *n = ez_node_succ(w);n;w=n,n = ez_node_succ(w)) { @@ -188,42 +190,152 @@ static void write_file_json(struct obstack *os, dbfile *file) { obstack_printf(os, ",\"lengthms\":\"%zd\"", file->duration / 1000); } +// TODO: This needs to be common with the player +const char * const list_names[4] = { + "all#shuffle", + NULL, + "jukebox", + "playnow" +}; + static void write_state_json(dbindex *db, struct obstack *os) { dbtxn *tx = dbindex_begin(db, NULL, 1); - dbstate state; - int res = dbstate_get(tx, db, &state); + int res; dbfile *file = NULL; - printf("state.fileid = %d\n", state.current.fileid); - printf("state.pos = %zd\n", state.pos); - printf("state.listid = %d\n", state.current.listid); - printf("state.seq = %d\n", state.current.seq); - - if (res == 0 - && (state.state & 1) - && (file = dbfile_get(tx, db, state.current.fileid))) { - obstack_1grow(os, '{'); - write_file_json(os, file); - - obstack_sgrow(os, ",\"path\":"); - write_value_json(os, file->path); - obstack_printf(os, ",\"status\":\"%s\"", (state.state & 2) ? "paused" : "playing"); - - obstack_printf(os, ",\"listid\":\"%u\"", state.current.listid); - obstack_printf(os, ",\"seq\":\"%u\"", state.current.seq); - obstack_printf(os, ",\"fileid\":\"%u\"", state.current.fileid); - - obstack_sgrow(os, ",\"position\":"); - duration_value(os, (uint64_t)(state.poss * 1000)); - obstack_printf(os, ",\"positionms\":\"%.0f\"", (state.poss * 1000)); - obstack_1grow(os, '}'); - } else { - obstack_sgrow(os, "{ 'status': 'idle' }"); + for (int i=ZPL_SIZEOF-1;i>=0;i--) { + dbstate state; + dbid_t listid; + + if (!list_names[i]) + continue; + + listid = dblistid_get_name(tx, db, list_names[i]); + res = dbstate_get(tx, db, listid, &state); + + if (res == 0 + && (state.state & 1) + && (file = dbfile_get(tx, db, state.curr.fileid))) { + + printf("state.fileid = %d\n", state.curr.fileid); + printf("state.pos = %zd\n", state.pos); + printf("state.listid = %d\n", state.curr.listid); + printf("state.seq = %d\n", state.curr.seq); + + obstack_1grow(os, '{'); + write_file_json(os, file); + + obstack_sgrow(os, ",\"path\":"); + //write_value_json(os, file->path); + write_value_json(os, dbfile_full_path(tx, db, file)); + + obstack_printf(os, ",\"status\":\"%s\"", (state.state & 2) ? "paused" : "playing"); + + obstack_printf(os, ",\"listid\":\"%u\"", state.curr.listid); + obstack_printf(os, ",\"seq\":\"%u\"", state.curr.seq); + obstack_printf(os, ",\"fileid\":\"%u\"", state.curr.fileid); + + obstack_sgrow(os, ",\"position\":"); + duration_value(os, (uint64_t)(state.poss * 1000)); + obstack_printf(os, ",\"positionms\":\"%.0f\"", (state.poss * 1000)); + obstack_1grow(os, '}'); + + dbfile_free(file); + goto done; + } } + obstack_sgrow(os, "{ \"status\": \"idle\" }"); +done: dbindex_abort(tx); +} - dbfile_free(file); +/* upcoming queue, maybe also go back a couple for history? */ +/* TODO: make this reusable for this, player, and index-util */ +static void write_queue_json(dbindex *db, struct obstack *io) { + dbtxn *tx = dbindex_begin(db, NULL, 1); + int limit = 10; + int count = 0; + int res; + dbstate states[ZPL_SIZEOF] = { 0 }; + int playing = -1; + + obstack_1grow(io, '{'); + + obstack_sgrow(io, "\"name\":"); + write_value_json(io, "queue"); + obstack_sgrow(io, ",\"items\": ["); + + for (int i=ZPL_SIZEOF-1;i>=0 && count < limit;i--) { + if (!list_names[i]) + continue; + + states[i].curr.listid = dblistid_get_name(tx, db, list_names[i]); + res = dbstate_get(tx, db, states[i].curr.listid, &states[i]); + + // Grab the playing item as the first entry + if (res == 0 && (states[i].state & 1) == 1 && count == 0) { + dbscan scan; + dblistcursor cursor = states[i].curr; + dbfile *file = dbscan_list_entry(tx, &scan, db, &cursor); + + if (file && dbfile_full_path(tx, db, file)) { + obstack_sgrow(io, count > 0 ? ",{" : "{"); + write_file_json(io, file); + + obstack_printf(io, ",\"listid\":\"%u\"", cursor.listid); + obstack_printf(io, ",\"seq\":\"%u\"", cursor.seq); + + obstack_1grow(io, '}'); + + playing = i; + count += 1; + } + dbfile_free(file); + dbscan_free(&scan); + } + } + + for (int i=ZPL_SIZEOF-1;i>=0 && count < limit;i--) { + dbfile *file; + dbscan scan; + dblistcursor cursor = states[i].curr; + int scancount = 0; + + loop: + file = dbscan_list_entry(tx, &scan, db, &cursor); + while (count < limit && file) { + if (dbfile_full_path(tx, db, file) + && !(count == 1 && playing == i && scancount == 0)) { + obstack_sgrow(io, count > 0 ? ",{" : "{"); + write_file_json(io, file); + + obstack_printf(io, ",\"listid\":\"%u\"", cursor.listid); + obstack_printf(io, ",\"seq\":\"%u\"", cursor.seq); + + obstack_1grow(io, '}'); + count++; + } + + dbfile_free(file); + file = dbscan_list_entry_next(&scan, &cursor); + scancount++; + } + dbfile_free(file); + res = dbscan_result(&scan); + dbscan_free(&scan); + + // loop on default playlist always + if (res == MDB_NOTFOUND) { + if (i == 0 && cursor.seq > 0) { + cursor.seq = 0; + cursor.fileid = 0; + goto loop; + } + } + } + obstack_sgrow(io, "]}"); + dbindex_abort(tx); } static int write_lists_json(dbindex *db, struct obstack *io) { @@ -249,6 +361,7 @@ static int write_lists_json(dbindex *db, struct obstack *io) { } obstack_printf(io, ",\"size\":\"%d\"", list->size); obstack_1grow(io, '}'); + dblist_free(list); } obstack_1grow(io, ']'); obstack_1grow(io, '}'); @@ -463,6 +576,28 @@ static int handle_lists(struct ez_httprequest *req, struct ez_httpresponse *rep) return 0; } +static int handle_queue(struct ez_httprequest *req, struct ez_httpresponse *rep) { + struct obstack *os = &rep->http.conn->os; + + if (strcmp(req->method, "GET") == 0) { + write_queue_json(db, os); + set_data_json(rep); + } else if (strcmp(req->method, "POST") == 0) { + // TODO: enqueue + int fid = param_int(req, "f", 0); + + if (fid) { + httpresponse_set_response(rep, 501, "Not implemented"); + } else { + httpresponse_set_response(rep, 400, "Missing name"); + } + } else { + httpresponse_set_response(rep, 400, "Invalid method"); + } + + return 0; +} + static void http_addheader(struct ez_http *http, const char *name, const char *value) { struct obstack *os = &http->conn->os; ez_pair *location = obstack_alloc(os, sizeof(*location)); @@ -591,6 +726,7 @@ static struct ez_httphandler handler_list[] = { { .path = "/x/goto", .fn = handle_goto }, { .path = "/x/status", .fn = handle_status }, + { .path = "/x/queue", .fn = handle_queue }, { .path = "/x/list", .fn = handle_lists }, { .path = "/x/list/", .fn = handle_list, .mode=EZ_PATH_PREFIX }, @@ -607,6 +743,8 @@ int main(int argc, char **argv) { player = notify_writer_new(NOTIFY_PLAYER); httpserver_init(&serv, 8000); + // TODO: param + serv.debug = 1; httpserver_addhandlers(&serv, handler_list, sizeof(handler_list)/sizeof(handler_list[0])); httpserver_run(&serv); httpserver_free(&serv); diff --git a/index-util.c b/index-util.c index 778795c..bb815bb 100644 --- a/index-util.c +++ b/index-util.c @@ -168,6 +168,14 @@ void search_suffix(const char *path) { /* ********************************************************************** */ +// TODO: This needs to be common with the player +const char * const list_names[4] = { + "all#shuffle", + NULL, + "jukebox", + "playnow" +}; + int main(int argc, char **argv) { const char *dbdir = MAIN_INDEX; dbid_t diskid = 0; @@ -327,6 +335,119 @@ int main(int argc, char **argv) { } else { printf("search failed\n"); } + } else if (strcmp(cmd, "play-state") == 0) { + dbtxn *tx = dbindex_begin(db, NULL, 1); + + for (int i=ZPL_SIZEOF-1;i>=0;i--) { + dbstate state; + + if (!list_names[i]) + continue; + + state.curr.listid = dblistid_get_name(tx, db, list_names[i]); + res = dbstate_get(tx, db, state.curr.listid, &state); + if (res != 0) + continue; + printf("%4d: ver=%08x state=%08x stamp=%016lx pos=%9.3fs lid=%4d seq=%4d fid=%5d\n", + i, state.version, state.state, state.stamp, state.poss, + state.curr.listid, state.curr.seq, state.curr.fileid); + } + dbindex_commit(tx); + } else if (strcmp(cmd, "play-queue") == 0) { + dbtxn *tx = dbindex_begin(db, NULL, 1); + int res; + int count = 0; + int limit = 100; + + for (int i=ZPL_SIZEOF-1;i>=0 && count < limit;i--) { + dbstate state; + dbscan scan; + dbfile *file; + + if (!list_names[i]) + continue; + + state.curr.listid = dblistid_get_name(tx, db, list_names[i]); + res = dbstate_get(tx, db, state.curr.listid, &state); + if (res != 0) + continue; + loop: + file = dbscan_list_entry(tx, &scan, db, &state.curr); + while (count < limit && file) { + if (dbfile_full_path(tx, db, file)) { + printf("%4d: fid=%4d lid=%4d seq=%4d title: %-60s %s\n", count++, file->id, state.curr.listid, state.curr.seq, file->title, file->path); + } + dbfile_free(file); + file = dbscan_list_entry_next(&scan, &state.curr); + } + dbfile_free(file); + res = dbscan_result(&scan); + dbscan_free(&scan); + + // loop on default playlist always + if (res == MDB_NOTFOUND) { + if (i == 0 && state.curr.seq > 0) { + state.curr.seq = 0; + state.curr.fileid = 0; + goto loop; + } + } + + } + +#if 0 + res = dbstate_get(tx, db, &state); + if (res >= 0) { + int next = 1; + + info = state.curr; + file = dbfile_get(tx, db, info.fileid); + printf("%4d: fid=%4d lid=%4d seq=%4d title: %-60s %s\n", count++, file->id, info.listid, info.seq, file->title, file->path); + if (state.list_curr != ZPL_USER + && state.list[state.list_curr].listid != info.listid) { + info = state.list[state.list_curr]; + next = 0; + } + + while (count < limit) { + file = dbscan_list_entry(tx, &scan, db, &info); + if (file) { + if (next) { + dbfile_free(file); + file = dbscan_list_entry_next(&scan, &info); + } + while (file && count < limit) { + printf("%4d: fid=%4d lid=%4d seq=%4d title: %-60s %s\n", count++, file->id, info.listid, info.seq, file->title, file->path); + dbfile_free(file); + if (count < limit) { + file = dbscan_list_entry_next(&scan, &info); + } else { + file = NULL; + } + } + } + res = dbscan_result(&scan); + dbscan_free(&scan); + if (res == MDB_NOTFOUND) { + if (state.list_curr > 0) { + state.list_curr -= 1; + info = state.list[state.list_curr]; + next = 0; + } else if (info.listid == state.list[ZPL_DEFAULT].listid && info.seq == 0 && info.fileid == 0) { + break; + } else { + info.listid = state.list[ZPL_DEFAULT].listid; + info.seq = 0; + info.fileid = 0; + next = 0; + } + } + } + } else { + printf("state query failed\n"); + } +#endif + dbindex_commit(tx); } if (res) printf("error (%d): %s\n", res, mdb_strerror(res)); diff --git a/music-player.c b/music-player.c index e1e678b..40bc775 100644 --- a/music-player.c +++ b/music-player.c @@ -113,10 +113,9 @@ struct audio_player { // playlist management dbindex *index; dbfile *playing; - dbstate playing_state; - - // current track/playlist management - dblistcursor *list_active; // update in play now? + dbstate state[ZPL_SIZEOF]; + int state_curr; + dbid_t list_id[ZPL_SIZEOF]; int quit; int paused; @@ -130,12 +129,6 @@ struct audio_player { int file_seq; // currently active file, hmm, do i care? // delete >> - // per-file state - // << delete - pthread_t reader_id; - volatile int state; // read by thread to communicate shutdown - // delete >> - AVFormatContext *fc; AVCodecContext *cc; AVStream *audio; @@ -149,7 +142,7 @@ struct audio_player { // pre-frame state //AVFrame *frame;// unused? - int64_t pos; // current pos in audio stream units + int64_t pos; // current pos in audio stream units, copied over to playing_state.pos char *data[1]; // data pointers for current output, planar or not, may point to 'buffer' int nsamples; // nsamples remaining for output @@ -171,6 +164,14 @@ static struct audio_player *audio_player_new(const char *device) { #endif audio_init_mixer(ap); + // Load list constants + dbtxn *tx = dbindex_begin(ap->index, NULL, 1); + ap->list_id[ZPL_DEFAULT] = dblistid_get_name(tx, ap->index, "all#shuffle"); + ap->list_id[ZPL_USER] = 0; // not implemented yet + ap->list_id[ZPL_JUKEBOX] = dblistid_get_name(tx, ap->index, "jukebox"); + ap->list_id[ZPL_PLAYNOW] = dblistid_get_name(tx, ap->index, "playnow"); + dbindex_abort(tx); + return ap; } @@ -555,6 +556,7 @@ static int audio_init_media(struct audio_player *ap, const char *path) { printf("audio_init_media '%s'\n", path); audio_close_media(ap); + ap->pos = 0; ap->fc = avformat_alloc_context(); res = avformat_open_input(&ap->fc, path, NULL, NULL); @@ -606,45 +608,28 @@ static int audio_init_media(struct audio_player *ap, const char *path) { static int audio_checkpoint_state(struct audio_player *ap) { dbtxn *tx = dbindex_begin(ap->index, NULL, 0); int res; + dbstate *state = &ap->state[ap->state_curr]; - if (tx) { - // TODO: meaningful state value - /* - if (ap->playing) { - ap->playing_state.state = 1; - ap->playing_state.fileid = ap->playing->id; - } else { - // ? - ap->playing_state.state = 0; - ap->playing_state.fileid = 0; - }*/ - if (ap->playing) { - ap->playing_state.state = 1 | (ap->paused ? 2 : 0); - // TODO: just update pos directly? - ap->playing_state.pos = ap->pos; - AVRational sb = ap->audio->time_base; - ap->playing_state.poss = av_q2d(sb) * ap->pos; - } else { - ap->playing_state.state = 0; - ap->playing_state.pos = 0; - ap->playing_state.poss = 0; - } - ap->playing_state.stamp = time(NULL); - - //printf("checkpoint state=%d file=%d pos=%zd poss=%f '%s'\n", ap->playing_state.state, ap->playing_state.current.fileid, ap->playing_state.pos, ap->playing_state.poss, ap->playing ? ap->playing->full_path : ""); - //printf("Checkpoint state, = %d\n", ap->playing_state.state); + if (ap->audio) { + state->state = (ap->playing ? 1 : 0) | (ap->paused ? 2 : 0); + state->pos = ap->pos; + state->poss = av_q2d(ap->audio->time_base) * ap->pos; + } else { + state->state = 0; + state->pos = 0; + state->poss = 0; + } + state->stamp = time(NULL); - if ((res = dbstate_put(tx, ap->index, &ap->playing_state)) == 0) { - dbindex_commit(tx); - } else { - printf("checkpoint failed: %s\n", mdb_strerror(res)); - dbindex_abort(tx); - } + res = dbstate_put(tx, ap->index, state->curr.listid, state); + if (res == 0){ + dbindex_commit(tx); } else { - printf("checkpoint failed to get tx\n"); + printf("checkpoint failed: %s\n", mdb_strerror(res)); + dbindex_abort(tx); } - return 0; + return res; } static void dump_cursor(const char *what, dblistcursor *info) { @@ -671,250 +656,175 @@ static void dump_cursor(const char *what, dblistcursor *info) { - current is just what is playing now - when it finishes the list state is copied to current and advanced */ -static int audio_advance_file(struct audio_player *ap, dbfile *(*advance)(dbscan *scan, dblistcursor *info)) { - int res; - int empty = ap->playing == NULL; - dbscan scan; - dbfile *file; - dbtxn *tx = dbindex_begin(ap->index, NULL, 1); - int retry = 0; + +// Find the current file and start it playing +static int audio_play_curr(struct audio_player *ap) { + dbtxn *tx = dbindex_begin(ap->index, NULL, 0); + int res = -1; dbfile_free(ap->playing); ap->playing = NULL; - // Playnow only runs once - if (ap->playing_state.list_mode == LIST_PLAYNOW) - ap->playing_state.list_mode = LIST_USER; - - // TODO: what what - ap->playing_state.list_mode = LIST_JUKEBOX; - - do { - dblistcursor *active; - - switch (ap->playing_state.list_mode) { - case LIST_DEFAULT: - default: - printf("trying default playlist\n"); - active = &ap->playing_state.list_default; - break; - case LIST_JUKEBOX: - printf("trying jukebox playlist\n"); - active = &ap->playing_state.list_jukebox; - break; - case LIST_USER: - printf("trying user playlist\n"); - active = &ap->playing_state.list_user; - break; - } - - dump_cursor("active?", active); + for (int i=ZPL_SIZEOF-1;i>=0;i--) { + dbstate *state = &ap->state[i]; + dbscan scan; + dbfile *file; - printf("loop: %d\n", retry); - file = dbscan_list_entry(tx, &scan, ap->index, active); - if (file) { - printf("entry: %s\n", file->path); - } else { - // repeat? reset? - printf("no entry found. scan.res =%d db.res=%d\n", dbscan_result(&scan), dbindex_result(ap->index)); - } + printf(" ? lid=%4d seq=%4d fid=%4d\n", state->curr.listid, state->curr.seq, state->curr.fileid); + retry: + file = dbscan_list_entry(tx, &scan, ap->index, &state->curr); while (file) { - dump_cursor("playing?", active); - - res = audio_init_media(ap, dbfile_full_path(tx, ap->index, file)); - - if (res == (-30798) && !empty) // && >loop? - res = 1; - - if (res == 0) { - ap->playing_state.current = *active; - ap->playing = file; - audio_checkpoint_state(ap); + if (dbfile_full_path(tx, ap->index, file)) { + res = audio_init_media(ap, file->full_path); + if (res == 0) { + // seek ? + ap->playing = file; + ap->state_curr = i; + + state->state = 1; + state->pos = 0; + state->poss = 0; + dbstate_put(tx, ap->index, state->curr.listid, state); + + printf("ok lid=%4d seq=%4d fid=%4d\n", state->curr.listid, state->curr.seq, state->curr.fileid); + + dbscan_free(&scan); + + // Ensure other states are not playing + for (int j=i-1;j>=0;j--) { + dbstate *s = &ap->state[j]; + if (s->curr.listid) { + s->state = 0; + dbstate_put(tx, ap->index, s->curr.listid, s); + } + } - // store advanced state as well - file = dbscan_list_entry_next(&scan, active); - if (file) { - dbfile_free(file); - } else { - active->seq += 1; - active->fileid = 0; + dbindex_commit(tx); + return res; } - printf("playing ok, next is lid=%d fid=%d seq=%d\n", active->listid, active->fileid, active->seq); - dbscan_free(&scan); - dbindex_abort(tx); - return res; } - dbfile_free(file); - file = advance(&scan, active); + file = dbscan_list_entry_next(&scan, &state->curr); } + state->state = 0; + dbstate_put(tx, ap->index, state->curr.listid, state); + res = dbscan_result(&scan); + dbscan_free(&scan); - // repeat at end of list? - // not sure on the trynext logic here - if (!file && dbscan_result(&scan) == -30798) { - switch (ap->playing_state.list_mode) { - case LIST_USER: - // if loop ... just loop - ap->playing_state.list_mode = LIST_JUKEBOX; - break; - case LIST_JUKEBOX: - ap->playing_state.list_mode = LIST_DEFAULT; - break; - case LIST_DEFAULT: - // loop the default list always - ap->playing_state.list_default.seq = 0; - ap->playing_state.list_default.fileid = 0; - break; + if (res == MDB_NOTFOUND) { + // loop on default playlist always + if (i == 0) { + if (state->curr.seq > 0) { + state->curr.seq = 0; + state->curr.fileid = 0; + goto retry; + } } - } else { - printf("worked?\n"); } - dbscan_free(&scan); - } while (!file && retry++ < 5); - - dbindex_abort(tx); - return res; -} + } -static int audio_next_file(struct audio_player *ap) { - return audio_advance_file(ap, dbscan_list_entry_next); + dbindex_commit(tx); + return -1; } -static int audio_prev_file(struct audio_player *ap) { - return audio_advance_file(ap, dbscan_list_entry_prev); -} // jump to a file (in a playlist) // TODO: if the playlist is the same should it jump to the next occurance of the file in the list? static int audio_goto_file(struct audio_player *ap, dblistcursor *info) { - int res = -1; // MDB_NOTFOUND dbtxn *tx = dbindex_begin(ap->index, NULL, 1); - dbfile *file = NULL; dbscan scan; + dbfile *file; - //if (info->listid != ap->current->listid) { - // now what? or do we just jump to that (user?) list instead? - // yes I think so! - //} - - printf("curret listid=%4d seq=%4d fileid=%4d\n", ap->playing_state.current.listid, ap->playing_state.current.seq, ap->playing_state.current.fileid); - printf("goto listid=%4d seq=%4d fileid=%4d\n", info->listid, info->seq, info->fileid); + if (ap->list_id[ap->state_curr] == info->listid) { + file = dbscan_list_entry(tx, &scan, ap->index, info); + if (file && file->id == info->fileid) { + ap->state[ap->state_curr].curr = *info; + audio_play_curr(ap); + } + } else { + // TODO: this should be ok if it's within the upcoming queue? + // but would have to advance all the other lists? + printf("Trying to jump out of the current playlist"); + } - // FIXME: file leaks if playing not set. - file = dbscan_list_entry(tx, &scan, ap->index, info); + dbindex_abort(tx); - if (!file || file->id != info->fileid) { - res = MDB_NOTFOUND; - goto fail; - } + return 0; +} - if ((res = audio_init_media(ap, dbfile_full_path(tx, ap->index, file)))) - goto fail; +static int audio_append(struct audio_player *ap, int start, dbid_t listid, dbid_t fileid) { + dbtxn *tx = dbindex_begin(ap->index, NULL, 0); + dbfile *file = dbfile_get(tx, ap->index, fileid); + dblistcursor info; + int res; - ap->playing = file; - file = NULL; - ap->playing_state.current = *info; - - // Work out the playlist mode now - if (info->listid == ap->playing_state.list_jukebox.listid) { - printf("jumping into jukebox list\n"); - ap->playing_state.list_jukebox = *info; - ap->playing_state.list_mode = LIST_JUKEBOX; - } else if (info->listid == ap->playing_state.list_default.listid) { - printf("jumping into default list\n"); - ap->playing_state.list_default = *info; - ap->playing_state.list_mode = LIST_DEFAULT; + if (file) { + info.listid = listid; + info.seq = 0; + info.fileid = fileid; + res = dblist_add_file(tx, ap->index, &info); } else { - printf("jumping to user list\n"); - ap->playing_state.list_user = *info; - ap->playing_state.list_mode = LIST_USER; + res = dbindex_result(ap->index); } - audio_checkpoint_state(ap); - printf("-> now listid=%4d seq=%4d fileid=%4d\n", info->listid, info->seq, info->fileid); -fail: - dbscan_free(&scan); - dbindex_abort(tx); + if (res == 0) + dbindex_commit(tx); + else + dbindex_abort(tx); + + if (res == 0 && start) + audio_play_curr(ap); return res; } // play a specific file right now out of band static int audio_play_now(struct audio_player *ap, int fileid) { - int res = -1; // MDB_NOTFOUND dbtxn *tx = dbindex_begin(ap->index, NULL, 0); dbfile *file = dbfile_get(tx, ap->index, fileid); - - printf("player goto: %d diskid=%d\n", fileid, file->diskid); + dblistcursor info; + int res; if (file) { - // add to end of the playnow playlist - ap->playing_state.list_playnow.seq = 0; - ap->playing_state.list_playnow.fileid = fileid; - dblist_add_file(tx, ap->index, &ap->playing_state.list_playnow); // ignore result? - - res = audio_init_media(ap, dbfile_full_path(tx, ap->index, file)); - if (res == 0) { - ap->playing = file; + info.listid = ap->list_id[ZPL_PLAYNOW]; + info.seq = 0; + info.fileid = fileid; + res = dblist_add_file(tx, ap->index, &info); - ap->playing_state.list_mode = LIST_PLAYNOW; - ap->playing_state.current = ap->playing_state.list_playnow; - printf("playing %s\n", file->path); - } else { - dbfile_free(file); - } + // Force jumping to end of playnow list + ap->state[ZPL_PLAYNOW].curr.seq = info.seq; + res = dbstate_put(tx, ap->index, info.listid, &ap->state[ZPL_PLAYNOW]); + } else { + res = dbindex_result(ap->index); } - if (res == 0) { + if (res == 0) dbindex_commit(tx); - // separate write transaction - // FIXME: put in same transaction? - audio_checkpoint_state(ap); - } else + else dbindex_abort(tx); + if (res == 0) + audio_play_curr(ap); + return res; } // jukebox // this adds the song to the jukebox playlist but doesn't jump immediately -// however ... "next song" will jump over it, so ... sigh +// TODO: this doesn't need to be in player static int audio_play_enqueue(struct audio_player *ap, int fileid) { - int res = -1; // MDB_NOTFOUND dbtxn *tx = dbindex_begin(ap->index, NULL, 0); dbfile *file = dbfile_get(tx, ap->index, fileid); - - printf("player enqueue: %d diskid=%d\n", fileid, file->diskid); + dblistcursor info; + int res; if (file) { - dblistcursor info; - - // add to end of the jukebox playlist - info.listid = ap->playing_state.list_jukebox.listid; + info.listid = ap->list_id[ZPL_JUKEBOX]; info.seq = 0; info.fileid = fileid; - if ((res = dblist_add_file(tx, ap->index, &info)) == 0) { - printf("enqueued listid=%4d seq=%4d fileid=%4d\n", - info.listid, - info.seq, - info.fileid); - - // If we're not already in jukebox mode, set the jukebox state to point - // to the previous file so that 'next file' will find the right one - // FIXME: what about 'prev' file? - // FIXME: find the actual seq in the db (last entry before this is added) - // FIXME: this will drop the last song on restore, perhaps it should have a next_list_mode instead - // this would also fix the same problem for play-now - - if (ap->playing_state.list_mode != LIST_JUKEBOX) { - printf(" set play state to jukebox\n"); - ap->playing_state.list_jukebox.seq = info.seq -1; - ap->playing_state.list_jukebox.fileid = 0; - ap->playing_state.list_mode = LIST_JUKEBOX; - } else { - printf(" play state already jukebox\n"); - } - } + res = dblist_add_file(tx, ap->index, &info); + } else { + res = dbindex_result(ap->index); } if (res == 0) @@ -925,69 +835,106 @@ static int audio_play_enqueue(struct audio_player *ap, int fileid) { return res; } -// first file on restarting server, see if we were playing something last time and start back there -// TODO: do something with the position -static int audio_restore_state(struct audio_player *ap) { - dbtxn *tx = dbindex_begin(ap->index, NULL, 1); - int res = -1; +static void audio_play_done(struct audio_player *ap) { + dbstate *state = &ap->state[ap->state_curr]; - // FIXME: playlist stuff - // FIXME: need to check current, etc - // Or just advance to next and let advance handle it? + dbfile_free(ap->playing); + ap->playing = NULL; - if ((res = dbstate_get(tx, ap->index, &ap->playing_state)) == 0 && ap->playing_state.state == 1) { - dbfile *file = dbfile_get(tx, ap->index, ap->playing_state.current.fileid); + state->curr.seq += 1; + audio_checkpoint_state(ap); +} - printf("restoring lid=%d fid=%d seq=%d file %s\n", - ap->playing_state.current.listid, - ap->playing_state.current.fileid, - ap->playing_state.current.seq, - file->path); - if (file) { - if (dbfile_full_path(tx, ap->index, file)) { - ap->playing = file; +static int audio_play_next(struct audio_player *ap) { + audio_play_done(ap); + return audio_play_curr(ap); +} + +// FIXME: this should use history, otherwise it gets stuck on list +static int audio_play_prev(struct audio_player *ap) { + dbtxn *tx = dbindex_begin(ap->index, NULL, 1); + dbstate *state = &ap->state[ap->state_curr]; + dbscan scan; + dbfile *file; + int res; + dbfile_free(ap->playing); + ap->playing = NULL; + + file = dbscan_list_entry(tx, &scan, ap->index, &state->curr); + if (file) { + dbfile_free(file); + file = dbscan_list_entry_prev(&scan, &state->curr); + while (file) { + if (dbfile_full_path(tx, ap->index, file)) { res = audio_init_media(ap, file->full_path); - // seek ? - } else { - dbfile_free(file); + if (res == 0) { + ap->playing = file; + + state->state = 1; + + goto found; + } } - } - } else { - // FIXME: check playing_state.state == 1 separate to error test above - res = -1; - printf("unable to restore state\n"); + dbfile_free(file); + file = dbscan_list_entry_prev(&scan, &state->curr); + } } - // always these - ap->playing_state.list_default.listid = dblistid_get_name(tx, ap->index, "all"); - ap->playing_state.list_playnow.listid = dblistid_get_name(tx, ap->index, "playnow"); - ap->playing_state.list_jukebox.listid = dblistid_get_name(tx, ap->index, "jukebox"); + res = -1; + state->state = 0; +found: + state->pos = 0; + state->poss = 0; + state->stamp = time(NULL); - { - dblist *list = dblist_get(tx, ap->index, ap->playing_state.current.listid); + dbstate_put(tx, ap->index, state->curr.listid, state); - printf("restore state=%d file=%d list='%s' '%s'\n", ap->playing_state.state, ap->playing_state.current.fileid, - ap->playing ? ap->playing->full_path : NULL, - list ? list->name : NULL); + dbscan_free(&scan); + dbindex_commit(tx); + + return res; +} + +// first file on restarting server, see if we were playing something last time and start back there +// TODO: do something with the position +static int audio_restore_state(struct audio_player *ap) { + dbtxn *tx = dbindex_begin(ap->index, NULL, 0); + int res = -1; - dblist_free(list); + printf("restore state\n"); + for (int i=0;istate[i]; + if (ap->list_id[i] == 0) + continue; + res = dbstate_get(tx, ap->index, ap->list_id[i], state); + if (res != 0) { + state->curr.listid = ap->list_id[i]; + state->curr.fileid = 0; + state->curr.seq = 0; + // Initialise sequence to end of list for playnow and jukebox + if (i == ZPL_PLAYNOW || i == ZPL_JUKEBOX) { + dblist *list = dblist_get(tx, ap->index, state->curr.listid); + + if (list) { + state->curr.seq = list->size + 1; + dblist_free(list); + } + } + dbstate_put(tx, ap->index, state->curr.listid, state); + } + printf(" state: lid=%4d seq=%4d fid=%4d\n", state->curr.listid, state->curr.seq, state->curr.fileid); } dbindex_commit(tx); - if (res == 0) - return res; - - return audio_next_file(ap); + return audio_play_curr(ap); } - - static void audio_player_control(struct audio_player *ap) { int ready = notify_msg_ready(ap->player); - if (time(NULL) != ap->playing_state.stamp) + if (ap->playing && time(NULL) != ap->state[ap->state_curr].stamp) audio_checkpoint_state(ap); #ifdef VOICE_MT @@ -1096,10 +1043,10 @@ static void audio_player_control(struct audio_player *ap) { } break; case NOTIFY_PLAY_NEXT: - audio_next_file(ap); + audio_play_next(ap); break; case NOTIFY_PLAY_PREV: - audio_prev_file(ap); + audio_play_prev(ap); break; case NOTIFY_PLAY_NOW: { struct notify_goto *g = msg; @@ -1276,7 +1223,8 @@ static void audio_player_loop(struct audio_player *ap) { res = av_read_frame(ap->fc, &pkt); if (res < 0) { audio_close_media(ap); - audio_next_file(ap); + audio_play_done(ap); + audio_play_curr(ap); continue; } diff --git a/player.html b/player.html index 9bb7f4e..62e1107 100644 --- a/player.html +++ b/player.html @@ -334,9 +334,9 @@ function playerUpdateList(status) { console.log("polling playlist"); - requestGET("/x/list/" + status.listid + "?f=" + status.id + "?s=" + status.seq, function(r) { - if (r.status === 200) { - let table = document.getElementById("coming-up"); + requestGET("/x/queue", function(r) { + if (r.status === 200) { + let table = document.getElementById("coming-up"); let items = document.getElementById("coming-up-list"); let list = JSON.parse(r.response); -- 2.39.2