OBJS += glwidget.moc.o mainwindow.moc.o vumeter.moc.o lrameter.moc.o correlation_meter.moc.o aboutdialog.moc.o
# Mixer objects
-OBJS += mixer.o pbo_frame_allocator.o context.o ref_counted_frame.o theme.o resampling_queue.o httpd.o ebu_r128_proc.o flags.o image_input.o stereocompressor.o filter.o alsa_output.o correlation_measurer.o
+OBJS += mixer.o pbo_frame_allocator.o context.o ref_counted_frame.o theme.o resampling_queue.o httpd.o ebu_r128_proc.o flags.o image_input.o stereocompressor.o filter.o alsa_output.o correlation_measurer.o disk_space_estimator.o
# Streaming and encoding objects
OBJS += quicksync_encoder.o x264_encoder.o x264_speed_control.o video_encoder.o metacube2.o mux.o audio_encoder.o ffmpeg_raii.o
--- /dev/null
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/vfs.h>
+#include <unistd.h>
+
+#include "disk_space_estimator.h"
+#include "timebase.h"
+
+DiskSpaceEstimator::DiskSpaceEstimator(DiskSpaceEstimator::callback_t callback)
+ : callback(callback)
+{
+}
+
+void DiskSpaceEstimator::report_write(const std::string &filename, uint64_t pts)
+{
+ if (filename != last_filename) {
+ last_filename = filename;
+ measure_points.clear();
+ }
+
+ // 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;
+ }
+
+ struct statfs fst;
+ if (statfs(filename.c_str(), &fst) == -1) {
+ perror(filename.c_str());
+ return;
+ }
+
+ if (!measure_points.empty()) {
+ off_t free_bytes = off_t(fst.f_bavail) * fst.f_frsize;
+ double bytes_per_second = double(st.st_size - measure_points.front().size) /
+ (pts - measure_points.front().pts) * TIMEBASE;
+ double seconds_left = free_bytes / bytes_per_second;
+ callback(free_bytes, seconds_left);
+ }
+
+ measure_points.push_back({ pts, st.st_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 <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 how much it grew since last time. Called by the Mux object
+ // responsible for writing to the stream on disk.
+ //
+ // 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;
+ std::string last_filename;
+
+ struct MeasurePoint {
+ uint64_t pts;
+ off_t size;
+ };
+ std::deque<MeasurePoint> measure_points;
+};
+
+extern DiskSpaceEstimator *global_disk_space_estimator;
+
+#endif // !defined(_DISK_SPACE_ESTIMATOR_H)
#include <QString>
#include "aboutdialog.h"
+#include "disk_space_estimator.h"
#include "flags.h"
#include "glwidget.h"
#include "lrameter.h"
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);
+
ui->me_live->set_output(Mixer::OUTPUT_LIVE);
ui->me_preview->set_output(Mixer::OUTPUT_PREVIEW);
ui->locut_cutoff_display->setText(buf);
}
+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.
+ });
+}
+
void MainWindow::limiter_threshold_knob_changed(int value)
{
float threshold_dbfs = value * 0.1f;
class MainWindow;
} // namespace Ui
+class QLabel;
class QPushButton;
class MainWindow : public QMainWindow
bool eventFilter(QObject *watched, QEvent *event) override;
void set_white_balance(int channel_number, int x, int y);
+ // Called from DiskSpaceEstimator.
+ void report_disk_space(off_t free_bytes, double estimated_seconds_left);
+
// Called from the mixer.
void audio_level_callback(float level_lufs, float peak_db, float global_level_lufs, float range_low_lufs, float range_high_lufs, float gain_staging_db, float final_makeup_gain_db, float correlation);
std::chrono::steady_clock::time_point last_audio_level_callback;
Ui::MainWindow *ui;
+ QLabel *disk_free_label;
QPushButton *transition_btn1, *transition_btn2, *transition_btn3;
std::vector<Ui::Display *> previews;
int current_wb_pick_display = -1;
#include "context.h"
#include "decklink_capture.h"
#include "defs.h"
+#include "disk_space_estimator.h"
#include "flags.h"
#include "video_encoder.h"
#include "pbo_frame_allocator.h"
display_chain->set_dither_bits(0); // Don't bother.
display_chain->finalize();
- video_encoder.reset(new VideoEncoder(resource_pool.get(), h264_encoder_surface, global_flags.va_display, WIDTH, HEIGHT, &httpd));
+ video_encoder.reset(new VideoEncoder(resource_pool.get(), h264_encoder_surface, global_flags.va_display, WIDTH, HEIGHT, &httpd, global_disk_space_estimator));
// Start listening for clients only once VideoEncoder has written its header, if any.
httpd.start(9095);
const AVFormatContext * const ctx;
};
-Mux::Mux(AVFormatContext *avctx, int width, int height, Codec video_codec, const string &video_extradata, const AVCodecParameters *audio_codecpar, int time_base)
- : avctx(avctx)
+Mux::Mux(AVFormatContext *avctx, int width, int height, Codec video_codec, const string &video_extradata, const AVCodecParameters *audio_codecpar, int time_base, std::function<void(int64_t)> write_callback)
+ : avctx(avctx), write_callback(write_callback)
{
avstream_video = avformat_new_stream(avctx, nullptr);
if (avstream_video == nullptr) {
}
av_packet_unref(&pkt_copy);
+
+ // Note: This will be wrong in the case of plugged packets, but that only happens
+ // for network streams, not for files, and write callbacks are only really relevant
+ // for files. (We don't want to do this from write_packet_or_die, as it only has
+ // the rescaled pts, which is unsuitable for callback.)
+ if (pkt.stream_index == 0 && write_callback != nullptr) {
+ write_callback(pts);
+ }
}
void Mux::write_packet_or_die(const AVPacket &pkt)
#include <libavformat/avio.h>
}
+#include <functional>
#include <mutex>
#include <queue>
#include <vector>
CODEC_NV12, // Uncompressed 4:2:0.
};
- // Takes ownership of avctx.
- Mux(AVFormatContext *avctx, int width, int height, Codec video_codec, const std::string &video_extradata, const AVCodecParameters *audio_codecpar, int time_base);
+ // Takes ownership of avctx. <write_callback> will be called every time
+ // a write has been made to the video stream (id 0), with the pts of
+ // the just-written frame. (write_callback can be nullptr.)
+ Mux(AVFormatContext *avctx, int width, int height, Codec video_codec, const std::string &video_extradata, const AVCodecParameters *audio_codecpar, int time_base, std::function<void(int64_t)> write_callback);
~Mux();
void add_packet(const AVPacket &pkt, int64_t pts, int64_t dts);
std::vector<AVPacket *> plugged_packets; // Protected by <mu>.
AVStream *avstream_video, *avstream_audio;
+
+ std::function<void(int64_t)> write_callback;
};
#endif // !defined(_MUX_H)
#include <condition_variable>
#include <cstddef>
#include <cstdint>
+#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include "audio_encoder.h"
#include "context.h"
#include "defs.h"
+#include "disk_space_estimator.h"
#include "ffmpeg_raii.h"
#include "flags.h"
#include "mux.h"
#include "x264_encoder.h"
using namespace std;
+using namespace std::placeholders;
class QOpenGLContext;
class QSurface;
class QuickSyncEncoderImpl {
public:
- QuickSyncEncoderImpl(const std::string &filename, movit::ResourcePool *resource_pool, QSurface *surface, const string &va_display, int width, int height, AVOutputFormat *oformat, X264Encoder *x264_encoder);
+ QuickSyncEncoderImpl(const std::string &filename, movit::ResourcePool *resource_pool, QSurface *surface, const string &va_display, int width, int height, AVOutputFormat *oformat, X264Encoder *x264_encoder, DiskSpaceEstimator *disk_space_estimator);
~QuickSyncEncoderImpl();
void add_audio(int64_t pts, vector<float> audio);
bool begin_frame(GLuint *y_tex, GLuint *cbcr_tex);
int frame_height;
int frame_width_mbaligned;
int frame_height_mbaligned;
+
+ DiskSpaceEstimator *disk_space_estimator;
};
// Supposedly vaRenderPicture() is supposed to destroy the buffer implicitly,
return 0;
}
-QuickSyncEncoderImpl::QuickSyncEncoderImpl(const std::string &filename, movit::ResourcePool *resource_pool, QSurface *surface, const string &va_display, int width, int height, AVOutputFormat *oformat, X264Encoder *x264_encoder)
- : current_storage_frame(0), resource_pool(resource_pool), surface(surface), x264_encoder(x264_encoder), frame_width(width), frame_height(height)
+QuickSyncEncoderImpl::QuickSyncEncoderImpl(const std::string &filename, movit::ResourcePool *resource_pool, QSurface *surface, const string &va_display, int width, int height, AVOutputFormat *oformat, X264Encoder *x264_encoder, DiskSpaceEstimator *disk_space_estimator)
+ : current_storage_frame(0), resource_pool(resource_pool), surface(surface), x264_encoder(x264_encoder), frame_width(width), frame_height(height), disk_space_estimator(disk_space_estimator)
{
file_audio_encoder.reset(new AudioEncoder(AUDIO_OUTPUT_CODEC_NAME, DEFAULT_AUDIO_OUTPUT_BIT_RATE, oformat));
open_output_file(filename);
string video_extradata = ""; // FIXME: See other comment about global headers.
AVCodecParametersWithDeleter audio_codecpar = file_audio_encoder->get_codec_parameters();
- file_mux.reset(new Mux(avctx, frame_width, frame_height, Mux::CODEC_H264, video_extradata, audio_codecpar.get(), TIMEBASE));
+ file_mux.reset(new Mux(avctx, frame_width, frame_height, Mux::CODEC_H264, video_extradata, audio_codecpar.get(), TIMEBASE,
+ std::bind(&DiskSpaceEstimator::report_write, disk_space_estimator, filename, _1)));
}
void QuickSyncEncoderImpl::encode_thread_func()
}
// Proxy object.
-QuickSyncEncoder::QuickSyncEncoder(const std::string &filename, movit::ResourcePool *resource_pool, QSurface *surface, const string &va_display, int width, int height, AVOutputFormat *oformat, X264Encoder *x264_encoder)
- : impl(new QuickSyncEncoderImpl(filename, resource_pool, surface, va_display, width, height, oformat, x264_encoder)) {}
+QuickSyncEncoder::QuickSyncEncoder(const std::string &filename, movit::ResourcePool *resource_pool, QSurface *surface, const string &va_display, int width, int height, AVOutputFormat *oformat, X264Encoder *x264_encoder, DiskSpaceEstimator *disk_space_estimator)
+ : impl(new QuickSyncEncoderImpl(filename, resource_pool, surface, va_display, width, height, oformat, x264_encoder, disk_space_estimator)) {}
// Must be defined here because unique_ptr<> destructor needs to know the impl.
QuickSyncEncoder::~QuickSyncEncoder() {}
#include <stdbool.h>
#include <stdint.h>
#include <atomic>
+#include <functional>
#include <memory>
#include <string>
#include <vector>
#include "ref_counted_gl_sync.h"
class AudioEncoder;
+class DiskSpaceEstimator;
class Mux;
class QSurface;
class QuickSyncEncoderImpl;
// .cpp file.
class QuickSyncEncoder {
public:
- QuickSyncEncoder(const std::string &filename, movit::ResourcePool *resource_pool, QSurface *surface, const std::string &va_display, int width, int height, AVOutputFormat *oformat, X264Encoder *x264_encoder);
+ QuickSyncEncoder(const std::string &filename, movit::ResourcePool *resource_pool, QSurface *surface, const std::string &va_display, int width, int height, AVOutputFormat *oformat, X264Encoder *x264_encoder, DiskSpaceEstimator *disk_space_estimator);
~QuickSyncEncoder();
void set_stream_mux(Mux *mux); // Does not take ownership. Must be called unless x264 is used for the stream.
} // namespace
-VideoEncoder::VideoEncoder(ResourcePool *resource_pool, QSurface *surface, const std::string &va_display, int width, int height, HTTPD *httpd)
- : resource_pool(resource_pool), surface(surface), va_display(va_display), width(width), height(height), httpd(httpd)
+VideoEncoder::VideoEncoder(ResourcePool *resource_pool, QSurface *surface, const std::string &va_display, int width, int height, HTTPD *httpd, DiskSpaceEstimator *disk_space_estimator)
+ : resource_pool(resource_pool), surface(surface), va_display(va_display), width(width), height(height), httpd(httpd), disk_space_estimator(disk_space_estimator)
{
oformat = av_guess_format(global_flags.stream_mux_name.c_str(), nullptr, nullptr);
assert(oformat != nullptr);
}
string filename = generate_local_dump_filename(/*frame=*/0);
- quicksync_encoder.reset(new QuickSyncEncoder(filename, resource_pool, surface, va_display, width, height, oformat, x264_encoder.get()));
+ quicksync_encoder.reset(new QuickSyncEncoder(filename, resource_pool, surface, va_display, width, height, oformat, x264_encoder.get(), disk_space_estimator));
open_output_stream();
stream_audio_encoder->add_mux(stream_mux.get());
qs_needing_cleanup.emplace_back(old_encoder);
}).detach();
- quicksync_encoder.reset(new QuickSyncEncoder(filename, resource_pool, surface, va_display, width, height, oformat, x264_encoder.get()));
+ quicksync_encoder.reset(new QuickSyncEncoder(filename, resource_pool, surface, va_display, width, height, oformat, x264_encoder.get(), disk_space_estimator));
quicksync_encoder->set_stream_mux(stream_mux.get());
}
}
int time_base = global_flags.stream_coarse_timebase ? COARSE_TIMEBASE : TIMEBASE;
- stream_mux.reset(new Mux(avctx, width, height, video_codec, video_extradata, stream_audio_encoder->get_codec_parameters().get(), time_base));
+ stream_mux.reset(new Mux(avctx, width, height, video_codec, video_extradata, stream_audio_encoder->get_codec_parameters().get(), time_base,
+ /*write_callback=*/nullptr));
}
int VideoEncoder::write_packet2_thunk(void *opaque, uint8_t *buf, int buf_size, AVIODataMarkerType type, int64_t time)
#include "ref_counted_frame.h"
#include "ref_counted_gl_sync.h"
+class DiskSpaceEstimator;
class HTTPD;
class QSurface;
class QuickSyncEncoder;
class VideoEncoder {
public:
- VideoEncoder(movit::ResourcePool *resource_pool, QSurface *surface, const std::string &va_display, int width, int height, HTTPD *httpd);
+ VideoEncoder(movit::ResourcePool *resource_pool, QSurface *surface, const std::string &va_display, int width, int height, HTTPD *httpd, DiskSpaceEstimator *disk_space_estimator);
~VideoEncoder();
void add_audio(int64_t pts, std::vector<float> audio);
std::string va_display;
int width, height;
HTTPD *httpd;
+ DiskSpaceEstimator *disk_space_estimator;
std::unique_ptr<Mux> stream_mux; // To HTTP.
std::unique_ptr<AudioEncoder> stream_audio_encoder;