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 db.o
+OBJS += vaapi_jpeg_decoder.o memcpy_interleaved.o db.o disk_space_estimator.o
OBJS += state.pb.o
%.o: %.cpp
--- /dev/null
+#include "disk_space_estimator.h"
+
+#include <stdio.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <memory>
+
+#include "timebase.h"
+
+DiskSpaceEstimator::DiskSpaceEstimator(DiskSpaceEstimator::callback_t callback)
+ : callback(callback)
+{
+}
+
+void DiskSpaceEstimator::report_write(const std::string &filename, uint64_t pts)
+{
+ // Reject points that are out-of-order (happens with B-frames).
+ if (!measure_points.empty() && pts <= measure_points.back().pts) {
+ return;
+ }
+
+ // Remove too old points.
+ while (measure_points.size() > 1 && measure_points.front().pts + window_length < pts) {
+ measure_points.pop_front();
+ }
+
+ struct stat st;
+ if (stat(filename.c_str(), &st) == -1) {
+ perror(filename.c_str());
+ return;
+ }
+
+ total_size += st.st_size;
+
+ struct statfs fst;
+ if (statfs(filename.c_str(), &fst) == -1) {
+ perror(filename.c_str());
+ return;
+ }
+
+ off_t free_bytes = off_t(fst.f_bavail) * fst.f_frsize;
+
+ if (!measure_points.empty()) {
+ double bytes_per_second = double(total_size - measure_points.front().size) /
+ (pts - measure_points.front().pts) * TIMEBASE;
+ double seconds_left = free_bytes / bytes_per_second;
+
+ // Only report every second, since updating the UI can be expensive.
+ if (last_pts_reported == 0 || pts - last_pts_reported >= TIMEBASE) {
+ callback(free_bytes, seconds_left);
+ last_pts_reported = pts;
+ }
+ }
+
+ measure_points.push_back({ pts, total_size });
+}
+
+DiskSpaceEstimator *global_disk_space_estimator = nullptr; // Created in MainWindow::MainWindow().
--- /dev/null
+#ifndef _DISK_SPACE_ESTIMATOR_H
+#define _DISK_SPACE_ESTIMATOR_H
+
+// A class responsible for measuring how much disk there is left when we
+// store our video to disk, and how much recording time that equates to.
+// It gets callbacks from the Mux writing the stream to disk (which also
+// knows which filesystem the file is going to), makes its calculations,
+// and calls back to the MainWindow, which shows it to the user.
+//
+// The bitrate is measured over a simple 30-second sliding window.
+
+#include <stdint.h>
+#include <sys/types.h>
+#include <deque>
+#include <functional>
+#include <string>
+
+#include "timebase.h"
+
+class DiskSpaceEstimator
+{
+public:
+ typedef std::function<void(off_t free_bytes, double estimated_seconds_left)> callback_t;
+ DiskSpaceEstimator(callback_t callback);
+
+ // Report that a video frame with the given pts has just been written
+ // to the given file, so the estimator should stat the file and see
+ // by big it is. (The file is assumed to hold only that single frame,
+ // unlike in Nageru, where it is a growing file.)
+ //
+ // If the filename changed since last time, the estimation is reset.
+ // <pts> is taken to be in TIMEBASE units (see timebase.h).
+ void report_write(const std::string &filename, uint64_t pts);
+
+private:
+ static constexpr int64_t window_length = 30 * TIMEBASE;
+
+ callback_t callback;
+
+ struct MeasurePoint {
+ uint64_t pts;
+ off_t size;
+ };
+ std::deque<MeasurePoint> measure_points;
+ uint64_t last_pts_reported = 0;
+ off_t total_size = 0;
+};
+
+extern DiskSpaceEstimator *global_disk_space_estimator;
+
+#endif // !defined(_DISK_SPACE_ESTIMATOR_H)
#include "clip_list.h"
#include "context.h"
#include "defs.h"
+#include "disk_space_estimator.h"
#include "mainwindow.h"
#include "ffmpeg_raii.h"
#include "httpd.h"
fwrite(pkt.data, pkt.size, 1, fp);
fclose(fp);
+ global_disk_space_estimator->report_write(filename, pts);
+
post_to_main_thread([pkt, pts] {
if (pkt.stream_index == 0) {
global_mainwindow->ui->input1_display->setFrame(pkt.stream_index, pts, /*interpolated=*/false);
#include "mainwindow.h"
#include "clip_list.h"
+#include "disk_space_estimator.h"
#include "player.h"
#include "post_to_main_thread.h"
#include "timebase.h"
#include <sqlite3.h>
using namespace std;
+using namespace std::placeholders;
MainWindow *global_mainwindow = nullptr;
ClipList *cliplist_clips;
global_mainwindow = this;
ui->setupUi(this);
+ global_disk_space_estimator = new DiskSpaceEstimator(bind(&MainWindow::report_disk_space, this, _1, _2));
+ disk_free_label = new QLabel(this);
+ disk_free_label->setStyleSheet("QLabel {padding-right: 5px;}");
+ ui->menuBar->setCornerWidget(disk_free_label);
+
StateProto state = db.get_state();
cliplist_clips = new ClipList(state.clip_list());
any_selected && selected->selectedRows().back().row() < int(playlist_clips->size()) - 1);
ui->play_btn->setEnabled(!playlist_clips->empty());
}
+
+void MainWindow::report_disk_space(off_t free_bytes, double estimated_seconds_left)
+{
+ char time_str[256];
+ if (estimated_seconds_left < 60.0) {
+ strcpy(time_str, "<font color=\"red\">Less than a minute</font>");
+ } else if (estimated_seconds_left < 1800.0) { // Less than half an hour: Xm Ys (red).
+ int s = lrintf(estimated_seconds_left);
+ int m = s / 60;
+ s %= 60;
+ snprintf(time_str, sizeof(time_str), "<font color=\"red\">%dm %ds</font>", m, s);
+ } else if (estimated_seconds_left < 3600.0) { // Less than an hour: Xm.
+ int m = lrintf(estimated_seconds_left / 60.0);
+ snprintf(time_str, sizeof(time_str), "%dm", m);
+ } else if (estimated_seconds_left < 36000.0) { // Less than ten hours: Xh Ym.
+ int m = lrintf(estimated_seconds_left / 60.0);
+ int h = m / 60;
+ m %= 60;
+ snprintf(time_str, sizeof(time_str), "%dh %dm", h, m);
+ } else { // More than ten hours: Xh.
+ int h = lrintf(estimated_seconds_left / 3600.0);
+ snprintf(time_str, sizeof(time_str), "%dh", h);
+ }
+ char buf[256];
+ snprintf(buf, sizeof(buf), "Disk free: %'.0f MB (approx. %s)", free_bytes / 1048576.0, time_str);
+
+ std::string label = buf;
+
+ post_to_main_thread([this, label]{
+ disk_free_label->setText(QString::fromStdString(label));
+ ui->menuBar->setCornerWidget(disk_free_label); // Need to set this again for the sizing to get right.
+ });
+}
+
#include <stdbool.h>
#include <sys/types.h>
+
+#include <QLabel>
#include <QMainWindow>
#include "db.h"
Ui::MainWindow *ui;
private:
+ QLabel *disk_free_label;
Player *preview_player, *live_player;
DB db;
void resizeEvent(QResizeEvent *event) override;
bool eventFilter(QObject *watched, QEvent *event) override;
+ void report_disk_space(off_t free_bytes, double estimated_seconds_left);
+
private slots:
void relayout();
};
</item>
</layout>
</widget>
+ <widget class="QMenuBar" name="menuBar">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>1038</width>
+ <height>22</height>
+ </rect>
+ </property>
+ <widget class="QMenu" name="menuFile">
+ <property name="title">
+ <string>&File</string>
+ </property>
+ <addaction name="actionQuit"/>
+ </widget>
+ <addaction name="menuFile"/>
+ </widget>
+ <action name="actionQuit">
+ <property name="text">
+ <string>Quit</string>
+ </property>
+ </action>
</widget>
<customwidgets>
<customwidget>