From b561d43a60201395f1354a585aa37670eda45883 Mon Sep 17 00:00:00 2001 From: "Steinar H. Gunderson" Date: Wed, 28 Sep 2016 23:56:23 +0200 Subject: [PATCH] Make the UI show free disk space, and a rough estimation of for how much longer we can record. --- Makefile | 2 +- disk_space_estimator.cpp | 54 ++++++++++++++++++++++++++++++++++++++++ disk_space_estimator.h | 50 +++++++++++++++++++++++++++++++++++++ mainwindow.cpp | 39 +++++++++++++++++++++++++++++ mainwindow.h | 5 ++++ mixer.cpp | 3 ++- mux.cpp | 12 +++++++-- mux.h | 9 +++++-- quicksync_encoder.cpp | 18 +++++++++----- quicksync_encoder.h | 4 ++- video_encoder.cpp | 11 ++++---- video_encoder.h | 4 ++- 12 files changed, 192 insertions(+), 19 deletions(-) create mode 100644 disk_space_estimator.cpp create mode 100644 disk_space_estimator.h diff --git a/Makefile b/Makefile index 6d2063e..5e9029d 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ OBJS=glwidget.o main.o mainwindow.o vumeter.o lrameter.o vu_common.o correlation 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 diff --git a/disk_space_estimator.cpp b/disk_space_estimator.cpp new file mode 100644 index 0000000..1cb7d6f --- /dev/null +++ b/disk_space_estimator.cpp @@ -0,0 +1,54 @@ +#include +#include +#include +#include + +#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(). diff --git a/disk_space_estimator.h b/disk_space_estimator.h new file mode 100644 index 0000000..b585bc3 --- /dev/null +++ b/disk_space_estimator.h @@ -0,0 +1,50 @@ +#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 + +#include +#include +#include + +#include "timebase.h" + +class DiskSpaceEstimator +{ +public: + typedef std::function 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. + // 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 measure_points; +}; + +extern DiskSpaceEstimator *global_disk_space_estimator; + +#endif // !defined(_DISK_SPACE_ESTIMATOR_H) diff --git a/mainwindow.cpp b/mainwindow.cpp index fec26f2..841952f 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -19,6 +19,7 @@ #include #include "aboutdialog.h" +#include "disk_space_estimator.h" #include "flags.h" #include "glwidget.h" #include "lrameter.h" @@ -95,6 +96,11 @@ MainWindow::MainWindow() 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); @@ -287,6 +293,39 @@ void MainWindow::cutoff_knob_changed(int value) 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, "Less than a minute"); + } 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), "%dm %ds", 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; diff --git a/mainwindow.h b/mainwindow.h index 31a2048..89c5e13 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -17,6 +17,7 @@ class Display; class MainWindow; } // namespace Ui +class QLabel; class QPushButton; class MainWindow : public QMainWindow @@ -55,11 +56,15 @@ private: 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 previews; int current_wb_pick_display = -1; diff --git a/mixer.cpp b/mixer.cpp index f8ad584..1a4b2cf 100644 --- a/mixer.cpp +++ b/mixer.cpp @@ -37,6 +37,7 @@ #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" @@ -165,7 +166,7 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards) 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); diff --git a/mux.cpp b/mux.cpp index df974f0..4f9f4d0 100644 --- a/mux.cpp +++ b/mux.cpp @@ -29,8 +29,8 @@ struct PacketBefore { 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 write_callback) + : avctx(avctx), write_callback(write_callback) { avstream_video = avformat_new_stream(avctx, nullptr); if (avstream_video == nullptr) { @@ -130,6 +130,14 @@ void Mux::add_packet(const AVPacket &pkt, int64_t pts, int64_t dts) } 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) diff --git a/mux.h b/mux.h index b60b02a..3f5007d 100644 --- a/mux.h +++ b/mux.h @@ -9,6 +9,7 @@ extern "C" { #include } +#include #include #include #include @@ -20,8 +21,10 @@ public: 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. 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 write_callback); ~Mux(); void add_packet(const AVPacket &pkt, int64_t pts, int64_t dts); @@ -45,6 +48,8 @@ private: std::vector plugged_packets; // Protected by . AVStream *avstream_video, *avstream_audio; + + std::function write_callback; }; #endif // !defined(_MUX_H) diff --git a/quicksync_encoder.cpp b/quicksync_encoder.cpp index cc4020f..4e97422 100644 --- a/quicksync_encoder.cpp +++ b/quicksync_encoder.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,7 @@ extern "C" { #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" @@ -50,6 +52,7 @@ extern "C" { #include "x264_encoder.h" using namespace std; +using namespace std::placeholders; class QOpenGLContext; class QSurface; @@ -206,7 +209,7 @@ FrameReorderer::Frame FrameReorderer::get_first_frame() 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 audio); bool begin_frame(GLuint *y_tex, GLuint *cbcr_tex); @@ -355,6 +358,8 @@ private: int frame_height; int frame_width_mbaligned; int frame_height_mbaligned; + + DiskSpaceEstimator *disk_space_estimator; }; // Supposedly vaRenderPicture() is supposed to destroy the buffer implicitly, @@ -1728,8 +1733,8 @@ int QuickSyncEncoderImpl::deinit_va() 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); @@ -1951,7 +1956,8 @@ void QuickSyncEncoderImpl::open_output_file(const std::string &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() @@ -2156,8 +2162,8 @@ void QuickSyncEncoderImpl::encode_frame(QuickSyncEncoderImpl::PendingFrame frame } // 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() {} diff --git a/quicksync_encoder.h b/quicksync_encoder.h index c1e844b..16fd3d1 100644 --- a/quicksync_encoder.h +++ b/quicksync_encoder.h @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,7 @@ extern "C" { #include "ref_counted_gl_sync.h" class AudioEncoder; +class DiskSpaceEstimator; class Mux; class QSurface; class QuickSyncEncoderImpl; @@ -57,7 +59,7 @@ class ResourcePool; // .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. diff --git a/video_encoder.cpp b/video_encoder.cpp index 0f3785f..dfbf565 100644 --- a/video_encoder.cpp +++ b/video_encoder.cpp @@ -35,8 +35,8 @@ string generate_local_dump_filename(int frame) } // 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); @@ -50,7 +50,7 @@ VideoEncoder::VideoEncoder(ResourcePool *resource_pool, QSurface *surface, const } 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()); @@ -92,7 +92,7 @@ void VideoEncoder::do_cut(int frame) 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()); } @@ -146,7 +146,8 @@ void VideoEncoder::open_output_stream() } 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) diff --git a/video_encoder.h b/video_encoder.h index fd16ac8..9a9f861 100644 --- a/video_encoder.h +++ b/video_encoder.h @@ -16,6 +16,7 @@ #include "ref_counted_frame.h" #include "ref_counted_gl_sync.h" +class DiskSpaceEstimator; class HTTPD; class QSurface; class QuickSyncEncoder; @@ -27,7 +28,7 @@ class ResourcePool; 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 audio); @@ -52,6 +53,7 @@ private: std::string va_display; int width, height; HTTPD *httpd; + DiskSpaceEstimator *disk_space_estimator; std::unique_ptr stream_mux; // To HTTP. std::unique_ptr stream_audio_encoder; -- 2.39.2