]> git.sesse.net Git - pkanalytics/blob - stats.cpp
Make it possible to insert events in the UI (very rudimentary yet, though).
[pkanalytics] / stats.cpp
1 #include <QMediaPlayer>
2 #include <QMainWindow>
3 #include <QApplication>
4 #include <QGridLayout>
5 #include <QVideoWidget>
6 #include <QShortcut>
7 #include <algorithm>
8 #include <string>
9 #include <map>
10 #include <vector>
11 #include <optional>
12 #include <sqlite3.h>
13 #include "mainwindow.h"
14 #include "ui_mainwindow.h"
15
16 using namespace std;
17
18 string format_timestamp(uint64_t pos)
19 {
20         int ms = pos % 1000;
21         pos /= 1000;
22         int sec = pos % 60;
23         pos /= 60;
24         int min = pos % 60;
25         int hour = pos / 60;
26
27         char buf[256];
28         snprintf(buf, sizeof(buf), "%d:%02d:%02d.%03d", hour, min, sec, ms);
29         return buf;
30 }
31
32 class EventsModel : public QAbstractTableModel
33 {
34 public:
35         EventsModel(sqlite3 *db) : db(db) {}
36
37         int rowCount(const QModelIndex &parent) const override
38         {
39                 refresh_if_needed();
40                 return events.size();
41         }
42         int columnCount(const QModelIndex &column) const override
43         {
44                 return 3;
45         }
46         QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
47         QVariant data(const QModelIndex &index, int role) const override;
48
49         int insert_event(uint64_t t, int player_id);
50
51 private:
52         struct Player {
53                 int player_id;
54                 string number;
55                 string name;
56         };
57         mutable map<int, Player> players;
58
59         struct Event {
60                 uint64_t t;
61                 optional<int> player_id;
62                 string type;
63         };
64         mutable vector<Event> events;
65         mutable bool stale = true;
66
67         sqlite3 *db;
68
69         void refresh_if_needed() const;
70 };
71
72 QVariant EventsModel::headerData(int section, Qt::Orientation orientation, int role) const
73 {
74         if (role != Qt::DisplayRole) {
75                 return QVariant();
76         }
77         if (orientation == Qt::Horizontal) {
78                 if (section == 0) {
79                         return "Time";
80                 } else if (section == 1) {
81                         return "Player";
82                 } else {
83                         return "Type";
84                 }
85         } else {
86                 return "";
87         }
88 }
89
90 QVariant EventsModel::data(const QModelIndex &index, int role) const
91 {
92         if (role != Qt::DisplayRole) {
93                 return QVariant();
94         }
95         refresh_if_needed();
96         if (index.column() == 0) {
97                 return QString::fromUtf8(format_timestamp(events[index.row()].t));
98         } else if (index.column() == 1) {
99                 optional<int> player_id = events[index.row()].player_id;
100                 if (player_id) {
101                         const Player &p = players[*player_id];
102                         return QString::fromUtf8(p.name + " (" + p.number + ")");
103                 } else {
104                         return QVariant();
105                 }
106         } else if (index.column() == 2) {
107                 return QString::fromUtf8(events[index.row()].type);
108         }
109         return QVariant();
110 }
111
112 void EventsModel::refresh_if_needed() const
113 {
114         if (!stale) {
115                 return;
116         }
117
118         players.clear();
119         events.clear();
120         stale = false;
121
122         // Read the players.
123         sqlite3_stmt *stmt;
124         int ret = sqlite3_prepare_v2(db, "SELECT player, number, name FROM player", -1, &stmt, 0);
125         if (ret != SQLITE_OK) {
126                 fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db));
127                 abort();
128         }
129         for ( ;; ) {
130                 ret = sqlite3_step(stmt);
131                 if (ret == SQLITE_ROW) {
132                         Player p;
133                         p.player_id = sqlite3_column_int(stmt, 0);
134                         p.number = (const char *)sqlite3_column_text(stmt, 1);
135                         p.name = (const char *) sqlite3_column_text(stmt, 2);
136                         players[p.player_id] = std::move(p);
137                 } else if (ret == SQLITE_DONE) {
138                         break;
139                 } else {
140                         fprintf(stderr, "SELECT step: %s\n", sqlite3_errmsg(db));
141                         abort();
142                 }
143         }
144         ret = sqlite3_finalize(stmt);
145         if (ret != SQLITE_OK) {
146                 fprintf(stderr, "SELECT finalize: %s\n", sqlite3_errmsg(db));
147                 abort();
148         }
149
150         // Read the events.
151         ret = sqlite3_prepare_v2(db, "SELECT t, player, type FROM event", -1, &stmt, 0);
152         if (ret != SQLITE_OK) {
153                 fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db));
154                 abort();
155         }
156         for ( ;; ) {
157                 ret = sqlite3_step(stmt);
158                 if (ret == SQLITE_ROW) {
159                         Event e;
160                         e.t = sqlite3_column_int(stmt, 0);
161                         e.player_id = sqlite3_column_int(stmt, 1);
162                         e.type = (const char *)sqlite3_column_text(stmt, 2);
163                         events.push_back(std::move(e));
164                 } else if (ret == SQLITE_DONE) {
165                         break;
166                 } else {
167                         fprintf(stderr, "SELECT step: %s\n", sqlite3_errmsg(db));
168                         abort();
169                 }
170         }
171         ret = sqlite3_finalize(stmt);
172         if (ret != SQLITE_OK) {
173                 fprintf(stderr, "SELECT finalize: %s\n", sqlite3_errmsg(db));
174                 abort();
175         }
176
177         // TODO what if data changes externally?
178         //emit dataChanged(QModelIndex(
179 }
180
181 int EventsModel::insert_event(uint64_t t, int player_id)
182 {
183         auto it = lower_bound(events.begin(), events.end(), t,
184                 [](const Event &e, uint64_t t) { return e.t < t; });
185         int pos = distance(events.begin(), it);
186         beginInsertRows(QModelIndex(), pos, pos + 1);
187
188         Event e;
189         e.t = t;
190         e.player_id = player_id;
191         e.type = "unknown";
192         events.insert(events.begin() + pos, e);
193
194         endInsertRows();
195
196         // FIXME sqlite
197
198         return pos;
199 }
200
201 MainWindow::MainWindow()
202 {
203         player = new QMediaPlayer;
204         //player->setSource(QUrl::fromLocalFile("/home/sesse/dev/stats/ultimate.mkv"));
205         player->setSource(QUrl::fromLocalFile("/home/sesse/dev/stats/ultimate-prores.mkv"));
206         player->play();
207
208         ui = new Ui::MainWindow;
209         ui->setupUi(this);
210
211         connect(player, &QMediaPlayer::positionChanged, [this](uint64_t pos) {
212                 ui->timestamp->setText(QString::fromUtf8(format_timestamp(pos)));
213                 if (buffered_seek) {
214                         player->setPosition(*buffered_seek);
215                         buffered_seek.reset();
216                 }
217                 if (!playing) {
218                         player->pause();  // We only played to get a picture.
219                 }
220         });
221
222         player->setVideoOutput(ui->video);
223
224         connect(ui->minus10s, &QPushButton::clicked, [this]() { seek(-10000); });
225         connect(ui->plus10s, &QPushButton::clicked, [this]() { seek(10000); });
226
227         connect(ui->minus2s, &QPushButton::clicked, [this]() { seek(-2000); });
228         connect(ui->plus2s, &QPushButton::clicked, [this]() { seek(2000); });
229
230         // TODO: Would be nice to actually have a frame...
231         connect(ui->minus1f, &QPushButton::clicked, [this]() { seek(-20); });
232         connect(ui->plus1f, &QPushButton::clicked, [this]() { seek(20); });
233
234         connect(ui->play_pause, &QPushButton::clicked, [this]() {
235                 if (playing) {
236                         player->pause();
237                         ui->play_pause->setText("Play (space)");
238                 } else {
239                         player->setPlaybackRate(1.0);
240                         player->play();
241                         ui->play_pause->setText("Pause (space)");
242                 }
243                 playing = !playing;
244
245                 // Needs to be set anew when we modify setText(), evidently.
246                 ui->play_pause->setShortcut(QCoreApplication::translate("MainWindow", "Space", nullptr));
247         });
248
249         connect(ui->player_1, &QPushButton::clicked, [this]() {
250                 ui->event_view->selectRow(model->insert_event(player->position(), 1));
251         });
252         connect(ui->player_2, &QPushButton::clicked, [this]() {
253                 ui->event_view->selectRow(model->insert_event(player->position(), 2));
254         });
255         connect(ui->player_3, &QPushButton::clicked, [this]() {
256                 ui->event_view->selectRow(model->insert_event(player->position(), 3));
257         });
258         connect(ui->player_4, &QPushButton::clicked, [this]() {
259                 ui->event_view->selectRow(model->insert_event(player->position(), 4));
260         });
261         connect(ui->player_5, &QPushButton::clicked, [this]() {
262                 ui->event_view->selectRow(model->insert_event(player->position(), 5));
263         });
264         connect(ui->player_6, &QPushButton::clicked, [this]() {
265                 ui->event_view->selectRow(model->insert_event(player->position(), 6));
266         });
267         connect(ui->player_7, &QPushButton::clicked, [this]() {
268                 ui->event_view->selectRow(model->insert_event(player->position(), 7));
269         });
270 }
271
272 void MainWindow::setModel(EventsModel *model)
273 {
274         ui->event_view->setModel(model);
275         this->model = model;
276 }
277
278 void MainWindow::seek(int64_t delta_ms)
279 {
280         int64_t current_pos = buffered_seek ? *buffered_seek : player->position();
281         uint64_t pos = max<int64_t>(current_pos + delta_ms, 0);
282         buffered_seek = pos;
283         if (!playing) {
284                 player->setPlaybackRate(0.01);
285                 player->play();  // Or Qt won't show the seek.
286         }
287 }
288
289 sqlite3 *open_db(const char *filename)
290 {
291         sqlite3 *db;
292         int ret = sqlite3_open(filename, &db);
293         if (ret != SQLITE_OK) {
294                 fprintf(stderr, "%s: %s\n", filename, sqlite3_errmsg(db));
295                 exit(1);
296         }
297
298         sqlite3_exec(db, R"(
299                 CREATE TABLE IF NOT EXISTS player (player INTEGER PRIMARY KEY, number VARCHAR, name VARCHAR);
300         )", nullptr, nullptr, nullptr);  // Ignore errors.
301
302         sqlite3_exec(db, R"(
303                 CREATE TABLE IF NOT EXISTS event (t INTEGER, player INTEGER, type VARCHAR, FOREIGN KEY (player) REFERENCES player(player));
304         )", nullptr, nullptr, nullptr);  // Ignore errors.
305
306         sqlite3_exec(db, "PRAGMA journal_mode=WAL", nullptr, nullptr, nullptr);  // Ignore errors.
307         sqlite3_exec(db, "PRAGMA synchronous=NORMAL", nullptr, nullptr, nullptr);  // Ignore errors.
308         return db;
309 }
310
311 int main(int argc, char *argv[])
312 {
313         QApplication app(argc, argv);
314         sqlite3 *db = open_db("ultimate.db");
315
316         MainWindow mainWindow;
317         mainWindow.setModel(new EventsModel(db));
318         mainWindow.resize(QSize(1280, 720));
319         mainWindow.show();
320
321         return app.exec();
322
323 }