Implementation of Recorder with dual PBO mechanism for best efficiency

and compatibility. Fixed user interface and avoid user creating multiple
recorders.
This commit is contained in:
brunoherbelin
2020-07-27 15:56:24 +02:00
parent 3f782736ac
commit 3bb3e66f55
6 changed files with 180 additions and 71 deletions

View File

@@ -116,17 +116,12 @@ glm::vec3 FrameBuffer::resolution() const
return glm::vec3(attrib_.viewport.x, attrib_.viewport.y, 0.f); return glm::vec3(attrib_.viewport.x, attrib_.viewport.y, 0.f);
} }
void FrameBuffer::bind() void FrameBuffer::begin()
{ {
if (!framebufferid_) if (!framebufferid_)
init(); init();
glBindFramebuffer(GL_FRAMEBUFFER, framebufferid_); glBindFramebuffer(GL_FRAMEBUFFER, framebufferid_);
}
void FrameBuffer::begin()
{
bind();
Rendering::manager().pushAttrib(attrib_); Rendering::manager().pushAttrib(attrib_);
@@ -155,6 +150,21 @@ void FrameBuffer::release()
glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0);
} }
void FrameBuffer::readPixels()
{
if (!framebufferid_)
return;
if (use_multi_sampling_)
glBindFramebuffer(GL_READ_FRAMEBUFFER, intermediate_framebufferid_);
else
glBindFramebuffer(GL_READ_FRAMEBUFFER, framebufferid_);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, attrib_.viewport.x, attrib_.viewport.y, (use_alpha_? GL_RGBA : GL_RGB), GL_UNSIGNED_BYTE, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
bool FrameBuffer::blit(FrameBuffer *other) bool FrameBuffer::blit(FrameBuffer *other)
{ {
if (!framebufferid_ || !other || !other->framebufferid_) if (!framebufferid_ || !other || !other->framebufferid_)

View File

@@ -18,8 +18,6 @@ public:
FrameBuffer(uint width, uint height, bool useAlpha = false, bool multiSampling = false); FrameBuffer(uint width, uint height, bool useAlpha = false, bool multiSampling = false);
~FrameBuffer(); ~FrameBuffer();
// bind the FrameBuffer as current to draw into
void bind();
// unbind any framebuffer object // unbind any framebuffer object
static void release(); static void release();
// Bind & push attribs to prepare draw // Bind & push attribs to prepare draw
@@ -29,6 +27,9 @@ public:
// blit copy to another, returns true on success // blit copy to another, returns true on success
bool blit(FrameBuffer *other); bool blit(FrameBuffer *other);
// bind the FrameBuffer in READ and perform glReadPixels
// return the size of the buffer
void readPixels();
// clear color // clear color
inline void setClearColor(glm::vec4 color) { attrib_.clear_color = color; } inline void setClearColor(glm::vec4 color) { attrib_.clear_color = color; }

View File

@@ -20,11 +20,16 @@
#include "Recorder.h" #include "Recorder.h"
// use glReadPixel or glGetTextImage ?
// read pixels & pbo should be the fastest
// https://stackoverflow.com/questions/38140527/glreadpixels-vs-glgetteximage
#define USE_GLREADPIXEL
using namespace std; using namespace std;
Recorder::Recorder() : finished_(false) Recorder::Recorder() : finished_(false), pbo_index_(0), pbo_next_index_(0), size_(0)
{ {
pbo_[0] = pbo_[1] = 0;
} }
PNGRecorder::PNGRecorder() : Recorder() PNGRecorder::PNGRecorder() : Recorder()
@@ -34,6 +39,7 @@ PNGRecorder::PNGRecorder() : Recorder()
path = SystemToolkit::home_path(); path = SystemToolkit::home_path();
filename_ = path + SystemToolkit::date_time_string() + "_vimix.png"; filename_ = path + SystemToolkit::date_time_string() + "_vimix.png";
} }
// Thread to perform slow operation of saving to file // Thread to perform slow operation of saving to file
@@ -44,7 +50,7 @@ void save_png(std::string filename, unsigned char *data, uint w, uint h, uint c)
// save file // save file
stbi_write_png(filename.c_str(), w, h, c, data, w * c); stbi_write_png(filename.c_str(), w, h, c, data, w * c);
// notify // notify
Log::Notify("Capture %s saved.", filename.c_str()); Log::Notify("Capture %s ready (%d x %d %d)", filename.c_str(), w, h, c);
// done // done
free(data); free(data);
} }
@@ -52,21 +58,68 @@ void save_png(std::string filename, unsigned char *data, uint w, uint h, uint c)
void PNGRecorder::addFrame(FrameBuffer *frame_buffer, float) void PNGRecorder::addFrame(FrameBuffer *frame_buffer, float)
{ {
// ignore
if (frame_buffer == nullptr)
return;
// get what is needed from frame buffer
uint w = frame_buffer->width(); uint w = frame_buffer->width();
uint h = frame_buffer->height(); uint h = frame_buffer->height();
uint c = frame_buffer->use_alpha() ? 4 : 3; uint c = frame_buffer->use_alpha() ? 4 : 3;
GLenum format = frame_buffer->use_alpha() ? GL_RGBA : GL_RGB;
uint size = w * h * c;
unsigned char * data = (unsigned char*) malloc(size);
glGetTextureSubImage( frame_buffer->texture(), 0, 0, 0, 0, w, h, 1, format, GL_UNSIGNED_BYTE, size, data); // first iteration: initialize and get frame
if (size_ < 1)
{
// init size
size_ = w * h * c;
// save in separate thread // create PBO
std::thread(save_png, filename_, data, w, h, c).detach(); glGenBuffers(2, pbo_);
// set writing PBO
glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo_[0]);
glBufferData(GL_PIXEL_PACK_BUFFER, size_, NULL, GL_STREAM_READ);
#ifdef USE_GLREADPIXEL
// get frame
frame_buffer->readPixels();
#else
glBindTexture(GL_TEXTURE_2D, frame_buffer->texture());
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGB, GL_UNSIGNED_BYTE, 0);
#endif
}
// second iteration; get frame and save file
else {
// set reading PBO
glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo_[0]);
// get pixels
unsigned char* ptr = (unsigned char*) glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
if (NULL != ptr) {
// prepare memory buffer0
unsigned char * data = (unsigned char*) malloc(size_);
// transfer frame to data
memmove(data, ptr, size_);
// save in separate thread
std::thread(save_png, filename_, data, w, h, c).detach();
}
// unmap
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
// ok done
glDeleteBuffers(2, pbo_);
// recorded one frame
finished_ = true;
}
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
// unsigned char * data = (unsigned char*) malloc(size);
// GLenum format = frame_buffer->use_alpha() ? GL_RGBA : GL_RGB;
// glGetTextureSubImage( frame_buffer->texture(), 0, 0, 0, 0, w, h, 1, format, GL_UNSIGNED_BYTE, size, data);
// record one frame only
finished_ = true;
} }
const char* VideoRecorder::profile_name[4] = { "H264 (low)", "H264 (high)", "Apple ProRes 4444", "WebM VP9" }; const char* VideoRecorder::profile_name[4] = { "H264 (low)", "H264 (high)", "Apple ProRes 4444", "WebM VP9" };
@@ -104,7 +157,7 @@ const std::vector<std::string> VideoRecorder::profile_description {
// "qtmux ! filesink name=sink"; // "qtmux ! filesink name=sink";
VideoRecorder::VideoRecorder() : Recorder(), frame_buffer_(nullptr), width_(0), height_(0), buf_size_(0), VideoRecorder::VideoRecorder() : Recorder(), frame_buffer_(nullptr), width_(0), height_(0),
recording_(false), pipeline_(nullptr), src_(nullptr), timestamp_(0), timeframe_(0), accept_buffer_(false) recording_(false), pipeline_(nullptr), src_(nullptr), timestamp_(0), timeframe_(0), accept_buffer_(false)
{ {
// auto filename // auto filename
@@ -121,12 +174,14 @@ VideoRecorder::VideoRecorder() : Recorder(), frame_buffer_(nullptr), width_(0),
VideoRecorder::~VideoRecorder() VideoRecorder::~VideoRecorder()
{ {
if (src_ != nullptr)
gst_object_unref (src_);
if (pipeline_ != nullptr) { if (pipeline_ != nullptr) {
gst_element_set_state (pipeline_, GST_STATE_NULL); gst_element_set_state (pipeline_, GST_STATE_NULL);
gst_object_unref (pipeline_); gst_object_unref (pipeline_);
} }
if (src_ != nullptr)
gst_object_unref (src_); glDeleteBuffers(2, pbo_);
} }
void VideoRecorder::addFrame (FrameBuffer *frame_buffer, float dt) void VideoRecorder::addFrame (FrameBuffer *frame_buffer, float dt)
@@ -146,7 +201,14 @@ void VideoRecorder::addFrame (FrameBuffer *frame_buffer, float dt)
// define stream properties // define stream properties
width_ = frame_buffer_->width(); width_ = frame_buffer_->width();
height_ = frame_buffer_->height(); height_ = frame_buffer_->height();
buf_size_ = width_ * height_ * (frame_buffer_->use_alpha() ? 4 : 3); size_ = width_ * height_ * (frame_buffer_->use_alpha() ? 4 : 3);
// create PBOs
glGenBuffers(2, pbo_);
glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo_[1]);
glBufferData(GL_PIXEL_PACK_BUFFER, size_, NULL, GL_STREAM_READ);
glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo_[0]);
glBufferData(GL_PIXEL_PACK_BUFFER, size_, NULL, GL_STREAM_READ);
// create a gstreamer pipeline // create a gstreamer pipeline
string description = "appsrc name=src ! videoconvert ! "; string description = "appsrc name=src ! videoconvert ! ";
@@ -164,18 +226,10 @@ void VideoRecorder::addFrame (FrameBuffer *frame_buffer, float dt)
} }
// setup file sink // setup file sink
sink_ = GST_BASE_SINK( gst_bin_get_by_name (GST_BIN (pipeline_), "sink") ); g_object_set (G_OBJECT (gst_bin_get_by_name (GST_BIN (pipeline_), "sink")),
if (sink_) { "location", filename_.c_str(),
g_object_set (G_OBJECT (sink_), "sync", FALSE,
"location", filename_.c_str(), NULL);
"sync", FALSE,
NULL);
}
else {
Log::Warning("VideoRecorder Could not configure file");
finished_ = true;
return;
}
// setup custom app source // setup custom app source
src_ = GST_APP_SRC( gst_bin_get_by_name (GST_BIN (pipeline_), "src") ); src_ = GST_APP_SRC( gst_bin_get_by_name (GST_BIN (pipeline_), "src") );
@@ -239,7 +293,7 @@ void VideoRecorder::addFrame (FrameBuffer *frame_buffer, float dt)
frame_buffer->use_alpha() != frame_buffer_->use_alpha()) { frame_buffer->use_alpha() != frame_buffer_->use_alpha()) {
stop(); stop();
Log::Info("Recording interrupted: new session (%d x %d) incompatible with recording (%d x %d)", frame_buffer->width(), frame_buffer->height(), width_, height_); Log::Warning("Recording interrupted: new session (%d x %d) incompatible with recording (%d x %d)", frame_buffer->width(), frame_buffer->height(), width_, height_);
} }
else { else {
// accepting a new frame buffer as input // accepting a new frame buffer as input
@@ -247,10 +301,8 @@ void VideoRecorder::addFrame (FrameBuffer *frame_buffer, float dt)
} }
} }
static int count = 0;
// store a frame if recording is active // store a frame if recording is active
if (recording_ && buf_size_ > 0) if (recording_ && size_ > 0)
{ {
// calculate dt in ns // calculate dt in ns
timeframe_ += gst_gdouble_to_guint64( dt * 1000000.f); timeframe_ += gst_gdouble_to_guint64( dt * 1000000.f);
@@ -259,28 +311,62 @@ void VideoRecorder::addFrame (FrameBuffer *frame_buffer, float dt)
// and if the encoder accepts data // and if the encoder accepts data
if ( timeframe_ > frame_duration_ - 3000000 && accept_buffer_) { if ( timeframe_ > frame_duration_ - 3000000 && accept_buffer_) {
GstBuffer *buffer = gst_buffer_new_and_alloc (buf_size_); // set buffer target for writing in a new frame
GLenum format = frame_buffer_->use_alpha() ? GL_RGBA : GL_RGB; glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo_[pbo_index_]);
// set timing of buffer #ifdef USE_GLREADPIXEL
buffer->pts = timestamp_; // get frame
buffer->duration = frame_duration_; frame_buffer->readPixels();
#else
glBindTexture(GL_TEXTURE_2D, frame_buffer->texture());
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGB, GL_UNSIGNED_BYTE, 0);
#endif
// OpenGL capture // update case ; alternating indices
GstMapInfo map; if ( pbo_next_index_ != pbo_index_ ) {
gst_buffer_map (buffer, &map, GST_MAP_WRITE);
glGetTextureSubImage( frame_buffer_->texture(), 0, 0, 0, 0, width_, height_, 1, format, GL_UNSIGNED_BYTE, buf_size_, map.data);
gst_buffer_unmap (buffer, &map);
// push // set buffer target for saving the frame
// Log::Info("VideoRecorder push data %ld", buffer->pts); glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo_[pbo_next_index_]);
gst_app_src_push_buffer (src_, buffer);
// NB: buffer will be unrefed by the appsrc
// restart counter // new buffer
GstBuffer *buffer = gst_buffer_new_and_alloc (size_);
// set timing of buffer
buffer->pts = timestamp_;
buffer->duration = frame_duration_;
// map gst buffer into a memory WRITE target
GstMapInfo map;
gst_buffer_map (buffer, &map, GST_MAP_WRITE);
// map PBO pixels into a memory READ pointer
unsigned char* ptr = (unsigned char*) glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
// transfer pixels from PBO memory to buffer memory
if (NULL != ptr)
memmove(map.data, ptr, size_);
// un-map
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
gst_buffer_unmap (buffer, &map);
// push
// Log::Info("VideoRecorder push data %ld", buffer->pts);
gst_app_src_push_buffer (src_, buffer);
// NB: buffer will be unrefed by the appsrc
// next timestamp
timestamp_ += frame_duration_;
}
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
// alternate indices
pbo_next_index_ = pbo_index_;
pbo_index_ = (pbo_index_ + 1) % 2;
// restart frame counter
timeframe_ = 0; timeframe_ = 0;
// next timestamp
timestamp_ += frame_duration_;
} }
} }
@@ -293,7 +379,6 @@ void VideoRecorder::addFrame (FrameBuffer *frame_buffer, float dt)
if (msg) { if (msg) {
// Log::Info("received EOS"); // Log::Info("received EOS");
// stop the pipeline // stop the pipeline
GstStateChangeReturn ret = gst_element_set_state (pipeline_, GST_STATE_NULL); GstStateChangeReturn ret = gst_element_set_state (pipeline_, GST_STATE_NULL);
if (ret == GST_STATE_CHANGE_FAILURE) if (ret == GST_STATE_CHANGE_FAILURE)
@@ -301,7 +386,6 @@ void VideoRecorder::addFrame (FrameBuffer *frame_buffer, float dt)
else else
Log::Notify("Recording %s ready.", filename_.c_str()); Log::Notify("Recording %s ready.", filename_.c_str());
count = 0;
finished_ = true; finished_ = true;
} }
} }

View File

@@ -29,7 +29,13 @@ public:
inline bool finished() const { return finished_; } inline bool finished() const { return finished_; }
protected: protected:
// thread-safe testing termination
std::atomic<bool> finished_; std::atomic<bool> finished_;
// PBO
guint pbo_[2];
guint pbo_index_, pbo_next_index_;
guint size_;
}; };
class PNGRecorder : public Recorder class PNGRecorder : public Recorder
@@ -52,7 +58,6 @@ class VideoRecorder : public Recorder
FrameBuffer *frame_buffer_; FrameBuffer *frame_buffer_;
uint width_; uint width_;
uint height_; uint height_;
uint buf_size_;
// operation // operation
std::atomic<bool> recording_; std::atomic<bool> recording_;
@@ -61,7 +66,6 @@ class VideoRecorder : public Recorder
// gstreamer pipeline // gstreamer pipeline
GstElement *pipeline_; GstElement *pipeline_;
GstAppSrc *src_; GstAppSrc *src_;
GstBaseSink *sink_;
GstClockTime timeframe_; GstClockTime timeframe_;
GstClockTime timestamp_; GstClockTime timestamp_;
GstClockTime frame_duration_; GstClockTime frame_duration_;

View File

@@ -24,6 +24,7 @@ Screenshot::Screenshot()
Screenshot::~Screenshot() Screenshot::~Screenshot()
{ {
glDeleteBuffers(1, &Pbo);
if (Data) free(Data); if (Data) free(Data);
} }

View File

@@ -57,6 +57,7 @@ using namespace std;
#include "TextEditor.h" #include "TextEditor.h"
static TextEditor editor; static TextEditor editor;
static Recorder *main_video_recorder = nullptr;
// utility functions // utility functions
void ShowAboutGStreamer(bool* p_open); void ShowAboutGStreamer(bool* p_open);
@@ -292,7 +293,14 @@ void UserInterface::handleKeyboard()
} }
else if (ImGui::IsKeyPressed( GLFW_KEY_R )) { else if (ImGui::IsKeyPressed( GLFW_KEY_R )) {
// toggle recording // toggle recording
Mixer::manager().session()->addRecorder(new VideoRecorder); if (main_video_recorder){
main_video_recorder->stop();
main_video_recorder = nullptr;
}
else {
main_video_recorder = new VideoRecorder;
Mixer::manager().session()->addRecorder(main_video_recorder);
}
} }
} }
@@ -871,9 +879,6 @@ void UserInterface::RenderPreview()
return; return;
} }
// adapt rendering if there is a recording ongoing
Recorder *rec = Mixer::manager().session()->frontRecorder();
// menu (no title bar) // menu (no title bar)
if (ImGui::BeginMenuBar()) if (ImGui::BeginMenuBar())
{ {
@@ -895,15 +900,19 @@ void UserInterface::RenderPreview()
ImGui::Separator(); ImGui::Separator();
// Stop recording menu if recording exists // Stop recording menu if recording exists
if (rec) { if (main_video_recorder) {
if ( ImGui::MenuItem( ICON_FA_SQUARE " Stop Record") ) if ( ImGui::MenuItem( ICON_FA_SQUARE " Stop Record", CTRL_MOD "R") ) {
rec->stop(); main_video_recorder->stop();
main_video_recorder = nullptr;
}
} }
// start recording menu // start recording menu
else { else {
if ( ImGui::MenuItem( ICON_FA_CIRCLE " Record") ) if ( ImGui::MenuItem( ICON_FA_CIRCLE " Record", CTRL_MOD "R") ) {
Mixer::manager().session()->addRecorder(new VideoRecorder); main_video_recorder = new VideoRecorder;
Mixer::manager().session()->addRecorder(main_video_recorder);
}
ImGui::SetNextItemWidth(300); ImGui::SetNextItemWidth(300);
ImGui::Combo("##RecProfile", &Settings::application.record.profile, VideoRecorder::profile_name, IM_ARRAYSIZE(VideoRecorder::profile_name) ); ImGui::Combo("##RecProfile", &Settings::application.record.profile, VideoRecorder::profile_name, IM_ARRAYSIZE(VideoRecorder::profile_name) );
@@ -948,13 +957,13 @@ void UserInterface::RenderPreview()
// preview image // preview image
ImGui::Image((void*)(intptr_t)output->texture(), imagesize); ImGui::Image((void*)(intptr_t)output->texture(), imagesize);
// recording indicator overlay // recording indicator overlay
if (rec) if (main_video_recorder)
{ {
float r = ImGui::GetTextLineHeightWithSpacing(); float r = ImGui::GetTextLineHeightWithSpacing();
ImGui::SetCursorScreenPos(ImVec2(draw_pos.x + r, draw_pos.y + r)); ImGui::SetCursorScreenPos(ImVec2(draw_pos.x + r, draw_pos.y + r));
ImGuiToolkit::PushFont(ImGuiToolkit::FONT_LARGE); ImGuiToolkit::PushFont(ImGuiToolkit::FONT_LARGE);
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0, 0.05, 0.05, 0.8f)); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0, 0.05, 0.05, 0.8f));
ImGui::Text(ICON_FA_CIRCLE " %s", rec->info().c_str() ); ImGui::Text(ICON_FA_CIRCLE " %s", main_video_recorder->info().c_str() );
ImGui::PopStyleColor(1); ImGui::PopStyleColor(1);
ImGui::PopFont(); ImGui::PopFont();
} }