From 58b5adcca3af1abbf4c69b00853bee037bb7fec7 Mon Sep 17 00:00:00 2001 From: "Steinar H. Gunderson" Date: Sun, 30 Sep 2018 23:40:46 +0200 Subject: [PATCH] Serialize state to disk between runs, using SQLite. --- .gitignore | 1 + Makefile | 12 +++-- clip_list.cpp | 65 ++++++++++++++++++++++++++ clip_list.h | 15 +++++- db.cpp | 95 ++++++++++++++++++++++++++++++++++++++ db.h | 20 ++++++++ mainwindow.cpp | 122 ++++++++++++++++++++++++++++++++++++------------- mainwindow.h | 24 +++++++++- state.proto | 18 ++++++++ 9 files changed, 333 insertions(+), 39 deletions(-) create mode 100644 db.cpp create mode 100644 db.h create mode 100644 state.proto diff --git a/.gitignore b/.gitignore index 5596e8e..ea67475 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ frames/*.jpeg eval flow vis +futatabi.db diff --git a/Makefile b/Makefile index a15bb6b..9e234ca 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ CXX=g++ -PKG_MODULES := Qt5Core Qt5Gui Qt5Widgets Qt5OpenGLExtensions Qt5OpenGL Qt5PrintSupport libjpeg movit libmicrohttpd +PROTOC=protoc +PKG_MODULES := Qt5Core Qt5Gui Qt5Widgets Qt5OpenGLExtensions Qt5OpenGL Qt5PrintSupport libjpeg movit libmicrohttpd protobuf sqlite3 CXXFLAGS ?= -O2 -g -Wall # Will be overridden by environment. CXXFLAGS += -fPIC $(shell pkg-config --cflags $(PKG_MODULES)) -DMOVIT_SHADER_DIR=\"$(shell pkg-config --variable=shaderdir movit)\" -pthread @@ -14,7 +15,8 @@ OBJS += $(OBJS_WITH_MOC:.o=.moc.o) OBJS += flow.o gpu_timers.o OBJS += ffmpeg_raii.o main.o player.o httpd.o mux.o metacube2.o video_stream.o context.o chroma_subsampler.o -OBJS += vaapi_jpeg_decoder.o memcpy_interleaved.o +OBJS += vaapi_jpeg_decoder.o memcpy_interleaved.o db.o +OBJS += state.pb.o %.o: %.cpp $(CXX) -MMD -MP $(CPPFLAGS) $(CXXFLAGS) -o $@ -c $< @@ -33,6 +35,10 @@ all: futatabi flow vis eval mainwindow.o: ui_mainwindow.h +clip_list.h: state.pb.h + +db.h: state.pb.h + futatabi: $(OBJS) $(CEF_LIBS) $(CXX) -o $@ $^ $(LDFLAGS) $(LDLIBS) flow: flow.o flow_main.o gpu_timers.o @@ -46,4 +52,4 @@ DEPS=$(OBJS:.o=.d) -include $(DEPS) clean: - $(RM) $(OBJS) $(DEPS) flow_main.o gpu_timers.o futatabi $(OBJS_WITH_MOC:.o=.moc.cpp) + $(RM) $(OBJS) $(DEPS) flow_main.o gpu_timers.o futatabi $(OBJS_WITH_MOC:.o=.moc.cpp) *.pb.cc *.pb.h diff --git a/clip_list.cpp b/clip_list.cpp index 69a6334..2f80575 100644 --- a/clip_list.cpp +++ b/clip_list.cpp @@ -325,6 +325,7 @@ void ClipList::add_clip(const Clip &clip) beginInsertRows(QModelIndex(), clips.size(), clips.size()); clips.push_back(clip); endInsertRows(); + emit any_content_changed(); } void PlayList::add_clip(const Clip &clip) @@ -332,6 +333,7 @@ void PlayList::add_clip(const Clip &clip) beginInsertRows(QModelIndex(), clips.size(), clips.size()); clips.push_back(clip); endInsertRows(); + emit any_content_changed(); } void PlayList::duplicate_clips(size_t first, size_t last) @@ -339,6 +341,7 @@ void PlayList::duplicate_clips(size_t first, size_t last) beginInsertRows(QModelIndex(), first, last); clips.insert(clips.begin() + first, clips.begin() + first, clips.begin() + last + 1); endInsertRows(); + emit any_content_changed(); } void PlayList::erase_clips(size_t first, size_t last) @@ -346,6 +349,7 @@ void PlayList::erase_clips(size_t first, size_t last) beginRemoveRows(QModelIndex(), first, last); clips.erase(clips.begin() + first, clips.begin() + last + 1); endRemoveRows(); + emit any_content_changed(); } void PlayList::move_clips(size_t first, size_t last, int delta) @@ -360,16 +364,19 @@ void PlayList::move_clips(size_t first, size_t last, int delta) rotate(clips.rbegin() + last - 1, clips.rbegin() + last, clips.rbegin() + first + 1); } endMoveRows(); + emit any_content_changed(); } void ClipList::emit_data_changed(size_t row) { emit dataChanged(index(row, 0), index(row, int(Column::NUM_COLUMNS))); + emit any_content_changed(); } void PlayList::emit_data_changed(size_t row) { emit dataChanged(index(row, 0), index(row, int(Column::NUM_COLUMNS))); + emit any_content_changed(); } void PlayList::set_currently_playing(int index, double progress) @@ -390,3 +397,61 @@ void PlayList::set_currently_playing(int index, double progress) emit dataChanged(this->index(index, column), this->index(index, column)); } } + +namespace { + +Clip deserialize_clip(const ClipProto &clip_proto) +{ + Clip clip; + clip.pts_in = clip_proto.pts_in(); + clip.pts_out = clip_proto.pts_out(); + for (int camera_idx = 0; camera_idx < min(clip_proto.description_size(), NUM_CAMERAS); ++camera_idx) { + clip.descriptions[camera_idx] = clip_proto.description(camera_idx); + } + clip.stream_idx = clip_proto.stream_idx(); + return clip; +} + +void serialize_clip(const Clip &clip, ClipProto *clip_proto) +{ + clip_proto->set_pts_in(clip.pts_in); + clip_proto->set_pts_out(clip.pts_out); + for (int camera_idx = 0; camera_idx < NUM_CAMERAS; ++camera_idx) { + *clip_proto->add_description() = clip.descriptions[camera_idx]; + } + clip_proto->set_stream_idx(clip.stream_idx); +} + +} // namespace + +ClipList::ClipList(const ClipListProto &serialized) +{ + for (const ClipProto &clip_proto : serialized.clip()) { + clips.push_back(deserialize_clip(clip_proto)); + } +} + +ClipListProto ClipList::serialize() const +{ + ClipListProto ret; + for (const Clip &clip : clips) { + serialize_clip(clip, ret.add_clip()); + } + return ret; +} + +PlayList::PlayList(const ClipListProto &serialized) +{ + for (const ClipProto &clip_proto : serialized.clip()) { + clips.push_back(deserialize_clip(clip_proto)); + } +} + +ClipListProto PlayList::serialize() const +{ + ClipListProto ret; + for (const Clip &clip : clips) { + serialize_clip(clip, ret.add_clip()); + } + return ret; +} diff --git a/clip_list.h b/clip_list.h index 2d37eab..31ddb6c 100644 --- a/clip_list.h +++ b/clip_list.h @@ -9,6 +9,7 @@ #include #include "defs.h" +#include "state.pb.h" struct Clip { int64_t pts_in = -1, pts_out = -1; // pts_in is inclusive, pts_out is exclusive. @@ -44,7 +45,7 @@ class ClipList : public QAbstractTableModel, public DataChangedReceiver { Q_OBJECT public: - ClipList() {} + explicit ClipList(const ClipListProto &serialized); enum class Column { IN, @@ -74,8 +75,13 @@ public: ClipProxy mutable_back() { return mutable_clip(size() - 1); } const Clip *back() const { return clip(size() - 1); } + ClipListProto serialize() const; + void emit_data_changed(size_t row) override; +signals: + void any_content_changed(); + private: std::vector clips; }; @@ -84,7 +90,7 @@ class PlayList : public QAbstractTableModel, public DataChangedReceiver { Q_OBJECT public: - PlayList() {} + explicit PlayList(const ClipListProto &serialized); enum class Column { PLAYING, @@ -123,8 +129,13 @@ public: void set_currently_playing(int index, double progress); // -1 = none. int get_currently_playing() const { return currently_playing_index; } + ClipListProto serialize() const; + void emit_data_changed(size_t row) override; +signals: + void any_content_changed(); + private: std::vector clips; int currently_playing_index = -1; diff --git a/db.cpp b/db.cpp new file mode 100644 index 0000000..c999a1d --- /dev/null +++ b/db.cpp @@ -0,0 +1,95 @@ +#include "db.h" + +#include + +using namespace std; + +DB::DB(const char *filename) +{ + int ret = sqlite3_open(filename, &db); + if (ret != SQLITE_OK) { + fprintf(stderr, "%s: %s\n", filename, sqlite3_errmsg(db)); + exit(1); + } + + sqlite3_exec(db, R"( + CREATE TABLE IF NOT EXISTS state (state BLOB); + )", nullptr, nullptr, nullptr); // Ignore errors. +} + +StateProto DB::get_state() +{ + StateProto state; + + sqlite3_stmt *stmt; + int ret = sqlite3_prepare(db, "SELECT state FROM state", -1, &stmt, 0); + if (ret != SQLITE_OK) { + fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db)); + exit(1); + } + + ret = sqlite3_step(stmt); + if (ret == SQLITE_ROW) { + bool ok = state.ParseFromArray(sqlite3_column_blob(stmt, 0), sqlite3_column_bytes(stmt, 0)); + if (!ok) { + fprintf(stderr, "State in database is corrupted!\n"); + exit(1); + } + } else if (ret != SQLITE_DONE) { + fprintf(stderr, "SELECT step: %s\n", sqlite3_errmsg(db)); + exit(1); + } + + ret = sqlite3_finalize(stmt); + if (ret != SQLITE_OK) { + fprintf(stderr, "SELECT finalize: %s\n", sqlite3_errmsg(db)); + exit(1); + } + + return state; +} + +void DB::store_state(const StateProto &state) +{ + string serialized; + state.SerializeToString(&serialized); + + int ret = sqlite3_exec(db, "BEGIN", nullptr, nullptr, nullptr); + if (ret != SQLITE_OK) { + fprintf(stderr, "BEGIN: %s\n", sqlite3_errmsg(db)); + exit(1); + } + + ret = sqlite3_exec(db, "DELETE FROM state", nullptr, nullptr, nullptr); + if (ret != SQLITE_OK) { + fprintf(stderr, "DELETE: %s\n", sqlite3_errmsg(db)); + exit(1); + } + + sqlite3_stmt *stmt; + ret = sqlite3_prepare(db, "INSERT INTO state VALUES (?)", -1, &stmt, 0); + if (ret != SQLITE_OK) { + fprintf(stderr, "INSERT prepare: %s\n", sqlite3_errmsg(db)); + exit(1); + } + + sqlite3_bind_blob(stmt, 1, serialized.data(), serialized.size(), SQLITE_STATIC); + + ret = sqlite3_step(stmt); + if (ret == SQLITE_ROW) { + fprintf(stderr, "INSERT step: %s\n", sqlite3_errmsg(db)); + exit(1); + } + + ret = sqlite3_finalize(stmt); + if (ret != SQLITE_OK) { + fprintf(stderr, "INSERT finalize: %s\n", sqlite3_errmsg(db)); + exit(1); + } + + ret = sqlite3_exec(db, "COMMIT", nullptr, nullptr, nullptr); + if (ret != SQLITE_OK) { + fprintf(stderr, "COMMIT: %s\n", sqlite3_errmsg(db)); + exit(1); + } +} diff --git a/db.h b/db.h new file mode 100644 index 0000000..826ab3f --- /dev/null +++ b/db.h @@ -0,0 +1,20 @@ +#ifndef DB_H +#define DB_H 1 + +#include "state.pb.h" +#include + +class DB { +public: + explicit DB(const char *filename); + DB(const DB &) = delete; + + StateProto get_state(); + void store_state(const StateProto &state); + +private: + StateProto state; + sqlite3 *db; +}; + +#endif // !defined(DB_H) diff --git a/mainwindow.cpp b/mainwindow.cpp index 0d9e80a..cfc9769 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -12,6 +12,9 @@ #include #include #include +#include + +#include using namespace std; @@ -24,16 +27,21 @@ extern mutex frame_mu; extern vector frames[MAX_STREAMS]; MainWindow::MainWindow() - : ui(new Ui::MainWindow) + : ui(new Ui::MainWindow), + db("futatabi.db") { global_mainwindow = this; ui->setupUi(this); - cliplist_clips = new ClipList(); + StateProto state = db.get_state(); + + cliplist_clips = new ClipList(state.clip_list()); ui->clip_list->setModel(cliplist_clips); + connect(cliplist_clips, &ClipList::any_content_changed, this, &MainWindow::content_changed); - playlist_clips = new PlayList(); + playlist_clips = new PlayList(state.play_list()); ui->playlist->setModel(playlist_clips); + connect(playlist_clips, &PlayList::any_content_changed, this, &MainWindow::content_changed); // For scrubbing in the pts columns. ui->clip_list->viewport()->installEventFilter(this); @@ -113,6 +121,10 @@ MainWindow::MainWindow() live_player_clip_progress(played_this_clip, total_length); }); }); + + defer_timeout = new QTimer(this); + defer_timeout->setSingleShot(true); + connect(defer_timeout, &QTimer::timeout, this, &MainWindow::defer_timer_expired); } void MainWindow::cue_in_clicked() @@ -252,6 +264,37 @@ void MainWindow::playlist_move(int delta) playlist_selection_changed(); } +void MainWindow::defer_timer_expired() +{ + state_changed(deferred_state); +} + +void MainWindow::content_changed() +{ + if (defer_timeout->isActive() && + (!currently_deferring_model_changes || deferred_change_id != current_change_id)) { + // There's some deferred event waiting, but this event is unrelated. + // So it's time to short-circuit that timer and do the work it wanted to do. + defer_timeout->stop(); + state_changed(deferred_state); + } + StateProto state; + *state.mutable_clip_list() = cliplist_clips->serialize(); + *state.mutable_play_list() = playlist_clips->serialize(); + if (currently_deferring_model_changes) { + deferred_change_id = current_change_id; + deferred_state = std::move(state); + defer_timeout->start(200); + return; + } + state_changed(state); +} + +void MainWindow::state_changed(const StateProto &state) +{ + db.store_state(state); +} + void MainWindow::play_clicked() { if (playlist_clips->empty()) return; @@ -405,13 +448,15 @@ bool MainWindow::eventFilter(QObject *watched, QEvent *event) } int64_t pts = scrub_pts_origin + adjusted_offset * scrub_sensitivity; - + currently_deferring_model_changes = true; if (scrub_type == SCRUBBING_CLIP_LIST) { ClipProxy clip = cliplist_clips->mutable_clip(scrub_row); if (scrub_column == int(ClipList::Column::IN)) { + current_change_id = "cliplist:in:" + to_string(scrub_row); set_pts_in(pts, current_pts, clip); preview_single_frame(pts, stream_idx, FIRST_AT_OR_AFTER); } else { + current_change_id = "cliplist:out" + to_string(scrub_row); pts = std::max(pts, clip->pts_in); pts = std::min(pts, current_pts); clip->pts_out = pts; @@ -420,15 +465,18 @@ bool MainWindow::eventFilter(QObject *watched, QEvent *event) } else { ClipProxy clip = playlist_clips->mutable_clip(scrub_row); if (scrub_column == int(PlayList::Column::IN)) { + current_change_id = "playlist:in:" + to_string(scrub_row); set_pts_in(pts, current_pts, clip); preview_single_frame(pts, clip->stream_idx, FIRST_AT_OR_AFTER); } else { + current_change_id = "playlist:out:" + to_string(scrub_row); pts = std::max(pts, clip->pts_in); pts = std::min(pts, current_pts); clip->pts_out = pts; preview_single_frame(pts, clip->stream_idx, LAST_BEFORE); } } + currently_deferring_model_changes = false; return true; // Don't use this mouse movement for selecting things. } @@ -456,41 +504,49 @@ bool MainWindow::eventFilter(QObject *watched, QEvent *event) int row = destination->rowAt(wheel->y()); if (column == -1 || row == -1) return false; - ClipProxy clip = (watched == ui->clip_list->viewport()) ? - cliplist_clips->mutable_clip(row) : playlist_clips->mutable_clip(row); - if (watched == ui->playlist->viewport()) { - stream_idx = clip->stream_idx; - } + currently_deferring_model_changes = true; + { + current_change_id = (watched == ui->clip_list->viewport()) ? "cliplist:" : "playlist:"; + ClipProxy clip = (watched == ui->clip_list->viewport()) ? + cliplist_clips->mutable_clip(row) : playlist_clips->mutable_clip(row); + if (watched == ui->playlist->viewport()) { + stream_idx = clip->stream_idx; + } - if (column != camera_column) { - last_mousewheel_camera_row = -1; - } - if (column == in_column) { - int64_t pts = clip->pts_in + wheel->angleDelta().y() * wheel_sensitivity; - set_pts_in(pts, current_pts, clip); - preview_single_frame(pts, stream_idx, FIRST_AT_OR_AFTER); - } else if (column == out_column) { - int64_t pts = clip->pts_out + wheel->angleDelta().y() * wheel_sensitivity; - pts = std::max(pts, clip->pts_in); - pts = std::min(pts, current_pts); - clip->pts_out = pts; - preview_single_frame(pts, stream_idx, LAST_BEFORE); - } else if (column == camera_column) { - int angle_degrees = wheel->angleDelta().y(); - if (last_mousewheel_camera_row == row) { - angle_degrees += leftover_angle_degrees; + if (column != camera_column) { + last_mousewheel_camera_row = -1; } + if (column == in_column) { + current_change_id += "in:" + to_string(row); + int64_t pts = clip->pts_in + wheel->angleDelta().y() * wheel_sensitivity; + set_pts_in(pts, current_pts, clip); + preview_single_frame(pts, stream_idx, FIRST_AT_OR_AFTER); + } else if (column == out_column) { + current_change_id += "out:" + to_string(row); + int64_t pts = clip->pts_out + wheel->angleDelta().y() * wheel_sensitivity; + pts = std::max(pts, clip->pts_in); + pts = std::min(pts, current_pts); + clip->pts_out = pts; + preview_single_frame(pts, stream_idx, LAST_BEFORE); + } else if (column == camera_column) { + current_change_id += "camera:" + to_string(row); + int angle_degrees = wheel->angleDelta().y(); + if (last_mousewheel_camera_row == row) { + angle_degrees += leftover_angle_degrees; + } - int stream_idx = clip->stream_idx + angle_degrees / camera_degrees_per_pixel; - stream_idx = std::max(stream_idx, 0); - stream_idx = std::min(stream_idx, NUM_CAMERAS - 1); - clip->stream_idx = stream_idx; + int stream_idx = clip->stream_idx + angle_degrees / camera_degrees_per_pixel; + stream_idx = std::max(stream_idx, 0); + stream_idx = std::min(stream_idx, NUM_CAMERAS - 1); + clip->stream_idx = stream_idx; - last_mousewheel_camera_row = row; - leftover_angle_degrees = angle_degrees % camera_degrees_per_pixel; + last_mousewheel_camera_row = row; + leftover_angle_degrees = angle_degrees % camera_degrees_per_pixel; - // Don't update the live view, that's rarely what the operator wants. + // Don't update the live view, that's rarely what the operator wants. + } } + currently_deferring_model_changes = false; } else if (event->type() == QEvent::MouseButtonRelease) { scrubbing = false; } diff --git a/mainwindow.h b/mainwindow.h index 24eea60..3621d32 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -5,7 +5,8 @@ #include #include -#include "clip_list.h" +#include "db.h" +#include "state.pb.h" namespace Ui { class MainWindow; @@ -25,6 +26,7 @@ public: private: Player *preview_player, *live_player; + DB db; // State when doing a scrub operation on a timestamp with the mouse. bool scrubbing = false; @@ -40,6 +42,22 @@ private: int last_mousewheel_camera_row = -1; int leftover_angle_degrees = 0; + // Some operations, notably scrubbing and scrolling, happen in so large increments + // that we want to group them instead of saving to disk every single time. + // If they happen (ie., we get a callback from the model that it's changed) while + // currently_deferring_model_changes, we fire off this timer. If it manages to elapse + // before some other event happens, we count the event. (If the other event is of the + // same kind, we just fire off the timer anew instead of taking any action.) + QTimer *defer_timeout; + std::string deferred_change_id; + StateProto deferred_state; + + // Before a change that should be deferred (see above), currently_deferring_model_changes + // must be set to true, and current_change_id must be given contents describing what's + // changed to avoid accidental grouping. + bool currently_deferring_model_changes = false; + std::string current_change_id; + void cue_in_clicked(); void cue_out_clicked(); void queue_clicked(); @@ -52,6 +70,10 @@ private: void playlist_remove(); void playlist_move(int delta); + void defer_timer_expired(); + void content_changed(); // In clip_list or play_list. + void state_changed(const StateProto &state); // Called post-filtering. + enum Rounding { FIRST_AT_OR_AFTER, LAST_BEFORE }; void preview_single_frame(int64_t pts, unsigned stream_idx, Rounding rounding); diff --git a/state.proto b/state.proto new file mode 100644 index 0000000..7fe78dc --- /dev/null +++ b/state.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +// Corresponds to struct Clip. +message ClipProto { + int64 pts_in = 1; + int64 pts_out = 2; + repeated string description = 3; + int64 stream_idx = 4; +} + +message ClipListProto { + repeated ClipProto clip = 1; +} + +message StateProto { + ClipListProto clip_list = 1; + ClipListProto play_list = 2; +} -- 2.39.2