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