Improved MediaPlayer performance by adding a dual-frame alternating

mechanism with mutex (read and write indices alternating between g-
streamer read thread and opengl write thread).
This commit is contained in:
brunoherbelin
2020-07-13 22:22:47 +02:00
parent 02d48eee10
commit c2e4eea7eb
2 changed files with 201 additions and 142 deletions

View File

@@ -1,5 +1,6 @@
#include "MediaPlayer.h" #include "MediaPlayer.h"
#include <algorithm> #include <algorithm>
#include <thread>
// vmix // vmix
#include "defines.h" #include "defines.h"
@@ -42,7 +43,6 @@ MediaPlayer::MediaPlayer(string name) : id_(name)
interlaced_ = false; interlaced_ = false;
enabled_ = true; enabled_ = true;
need_loop_ = false; need_loop_ = false;
v_frame_is_full_ = false;
rate_ = 1.0; rate_ = 1.0;
framerate_ = 0.0; framerate_ = 0.0;
@@ -55,13 +55,23 @@ MediaPlayer::MediaPlayer(string name) : id_(name)
desired_state_ = GST_STATE_PAUSED; desired_state_ = GST_STATE_PAUSED;
loop_ = LoopMode::LOOP_REWIND; loop_ = LoopMode::LOOP_REWIND;
current_segment_ = segments_.begin(); current_segment_ = segments_.begin();
v_frame_.buffer = nullptr;
// v_frame_.buffer = nullptr;
// v_frame_is_full_ = false;
vframe_write_index_ = 0;
vframe_read_index_ = 0;
for(guint i = 0; i < N_VFRAME; i++){
vframe_[i].buffer = nullptr;
vframe_is_full_[i] = false;
vframe_position_[i] = GST_CLOCK_TIME_NONE;
}
// no PBO by default // no PBO by default
pbo_[0] = pbo_[1] = 0; pbo_[0] = pbo_[1] = 0;
pbo_size_ = 0; pbo_size_ = 0;
pbo_index_ = 0; pbo_index_ = 0;
pbo_next_index_ = 1; pbo_next_index_ = 0;
textureindex_ = 0; textureindex_ = 0;
} }
@@ -224,15 +234,24 @@ void MediaPlayer::close()
} }
// cleanup eventual remaining frame related memory // cleanup eventual remaining frame related memory
if (v_frame_.buffer) // if (v_frame_.buffer)
gst_video_frame_unmap(&v_frame_); // gst_video_frame_unmap(&v_frame_);
for(guint i = 0; i < N_VFRAME; i++){
if (vframe_[i].buffer)
gst_video_frame_unmap(&vframe_[i]);
vframe_is_full_[i] = false;
vframe_position_[i] = GST_CLOCK_TIME_NONE;
}
// cleanup opengl texture // cleanup opengl texture
glDeleteTextures(1, &textureindex_); if (textureindex_)
glDeleteTextures(1, &textureindex_);
textureindex_ = 0; textureindex_ = 0;
// delete picture buffer // delete picture buffer
if (pbo_[0] || pbo_[1]) if (pbo_[0])
glDeleteBuffers(2, pbo_); glDeleteBuffers(2, pbo_);
pbo_size_ = 0; pbo_size_ = 0;
@@ -503,49 +522,39 @@ void MediaPlayer::init_texture()
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width_, height_, glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width_, height_,
0, GL_RGBA, GL_UNSIGNED_BYTE, v_frame_.data[0]); 0, GL_RGBA, GL_UNSIGNED_BYTE, vframe_[vframe_read_index_].data[0]);
if (!isimage_) { if (!isimage_) {
// need to fill image size // need to fill image size
pbo_size_ = height_ * width_ * 4; pbo_size_ = height_ * width_ * 4;
// create 2 pixel buffer objects, // create pixel buffer objects,
if (pbo_[0] || pbo_[1]) if (pbo_[0])
glDeleteBuffers(2, pbo_); glDeleteBuffers(2, pbo_);
glGenBuffers(2, pbo_); glGenBuffers(2, pbo_);
// create first PBO
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo_[0]);
// glBufferDataARB with NULL pointer reserves only memory space.
glBufferData(GL_PIXEL_UNPACK_BUFFER, pbo_size_, 0, GL_STREAM_DRAW);
// fill in with reset picture
GLubyte* ptr = (GLubyte*) glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
if (ptr) {
// update data directly on the mapped buffer
memmove(ptr, v_frame_.data[0], pbo_size_);
// release pointer to mapping buffer
glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
}
else {
// did not work, disable PBO
glDeleteBuffers(2, pbo_);
pbo_[0] = pbo_[1] = 0;
pbo_size_ = 0;
}
// idem with second PBO for(int i = 0; i < 2; i++ ) {
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo_[1]); // create PBO
glBufferData(GL_PIXEL_UNPACK_BUFFER, pbo_size_, 0, GL_STREAM_DRAW); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo_[i]);
ptr = (GLubyte*) glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY); // glBufferDataARB with NULL pointer reserves only memory space.
if (ptr) { glBufferData(GL_PIXEL_UNPACK_BUFFER, pbo_size_, 0, GL_STREAM_DRAW);
memmove(ptr, v_frame_.data[0], pbo_size_); // fill in with reset picture
glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); GLubyte* ptr = (GLubyte*) glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
} if (ptr) {
else { // update data directly on the mapped buffer
// did not work, disable PBO memmove(ptr, vframe_[vframe_read_index_].data[0], pbo_size_);
glDeleteBuffers(2, pbo_); // release pointer to mapping buffer
pbo_[0] = pbo_[1] = 0; glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
pbo_size_ = 0; }
else {
// did not work, disable PBO
glDeleteBuffers(4, pbo_);
pbo_[0] = pbo_[1] = 0;
pbo_size_ = 0;
break;
}
} }
// should be good to go, wrap it up // should be good to go, wrap it up
@@ -576,59 +585,75 @@ void MediaPlayer::update()
if (!enabled_) if (!enabled_)
return; return;
// apply texture // bind texture in any case (except if not initialized yet)
if (v_frame_is_full_) { if (textureindex_>0)
glBindTexture(GL_TEXTURE_2D, textureindex_);
// first occurence; create texture // try to access a vframe
if (textureindex_==0) { if (vframe_lock_[vframe_read_index_].try_lock() ) {
init_texture();
} // if we got a full vframe
// all other times, bind and fill texture if (vframe_is_full_[vframe_read_index_])
else
{ {
glBindTexture(GL_TEXTURE_2D, textureindex_);
// use dual Pixel Buffer Object // first occurence; create texture
if (pbo_size_ > 0) { if (textureindex_==0) {
// In dual PBO mode, increment current index first then get the next index init_texture();
pbo_index_ = (pbo_index_ + 1) % 2;
pbo_next_index_ = (pbo_index_ + 1) % 2;
// bind PBO to read pixels
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo_[pbo_index_]);
// copy pixels from PBO to texture object
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width_, height_, GL_RGBA, GL_UNSIGNED_BYTE, 0);
// glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width_, height_, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
// bind the next PBO to write pixels
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo_[pbo_next_index_]);
// See http://www.songho.ca/opengl/gl_pbo.html#map for more details
glBufferData(GL_PIXEL_UNPACK_BUFFER, pbo_size_, 0, GL_STREAM_DRAW);
// map the buffer object into client's memory
// GLubyte* ptr = (GLubyte*) glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, pbo_size_, GL_MAP_WRITE_BIT|GL_MAP_INVALIDATE_BUFFER_BIT);
GLubyte* ptr = (GLubyte*) glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
if (ptr) {
// update data directly on the mapped buffer
// NB : equivalent but faster (memmove instead of memcpy ?) than
// glNamedBufferSubData(pboIds[nextIndex], 0, imgsize, vp->getBuffer())
memmove(ptr, v_frame_.data[0], pbo_size_);
// release pointer to mapping buffer
glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
}
// done with PBO
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
} }
// all other times, fill the texture
else else
// without PBO, use standard opengl (slower) {
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width_, height_, // use dual Pixel Buffer Object
GL_RGBA, GL_UNSIGNED_BYTE, v_frame_.data[0]); if (pbo_size_ > 0) {
// In dual PBO mode, increment current index first then get the next index
pbo_index_ = (pbo_index_ + 1) % 2;
pbo_next_index_ = (pbo_index_ + 1) % 2;
// bind PBO to read pixels
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo_[pbo_index_]);
// copy pixels from PBO to texture object
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width_, height_, GL_RGBA, GL_UNSIGNED_BYTE, 0);
// bind the next PBO to write pixels
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo_[pbo_next_index_]);
// See http://www.songho.ca/opengl/gl_pbo.html#map for more details
glBufferData(GL_PIXEL_UNPACK_BUFFER, pbo_size_, 0, GL_STREAM_DRAW);
// map the buffer object into client's memory
GLubyte* ptr = (GLubyte*) glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
if (ptr) {
// update data directly on the mapped buffer
// NB : equivalent but faster (memmove instead of memcpy ?) than
// glNamedBufferSubData(pboIds[nextIndex], 0, imgsize, vp->getBuffer())
memmove(ptr, vframe_[vframe_read_index_].data[0], pbo_size_);
// release pointer to mapping buffer
glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
}
// done with PBO
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
}
else {
// without PBO, use standard opengl (slower)
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width_, height_,
GL_RGBA, GL_UNSIGNED_BYTE, vframe_[vframe_read_index_].data[0]);
}
}
// we just displayed a vframe : set position time to vframe PTS
position_ = vframe_position_[vframe_read_index_];
// sync with callback_pull_last_sample_video : inform its free
vframe_is_full_[vframe_read_index_] = false;
} }
// sync with callback_pull_last_sample_video vframe_lock_[vframe_read_index_].unlock();
v_frame_is_full_ = false;
} }
vframe_read_index_ = (vframe_read_index_ +1) %2;
// manage loop mode // manage loop mode
if (need_loop_ && !isimage_) { if (need_loop_ && !isimage_) {
execute_loop_command(); execute_loop_command();
@@ -767,39 +792,51 @@ double MediaPlayer::updateFrameRate() const
// CALLBACKS // CALLBACKS
bool MediaPlayer::fill_v_frame(GstBuffer *buf, bool ignorepts) bool MediaPlayer::fill_v_frame(GstBuffer *buf)
{ {
// always empty frame before filling it again
if (v_frame_.buffer)
gst_video_frame_unmap(&v_frame_);
// get the frame from buffer // non-blocking attempt to access vframe
if ( !gst_video_frame_map (&v_frame_, &v_frame_video_info_, buf, GST_MAP_READ ) ) { guint i = vframe_write_index_;
Log::Info("MediaPlayer %s Failed to map the video buffer", id_.c_str()); if ( vframe_lock_[i].try_lock()) {
return false;
}
// validate frame format // always empty frame before filling it again
if( GST_VIDEO_INFO_IS_RGB(&(v_frame_).info) && GST_VIDEO_INFO_N_PLANES(&(v_frame_).info) == 1) { if (vframe_[i].buffer)
gst_video_frame_unmap(&vframe_[vframe_write_index_]);
// validate time
if (ignorepts || position_ != buf->pts)
{
// got a new RGB frame ! // get the frame from buffer
v_frame_is_full_ = true; if ( !gst_video_frame_map (&vframe_[vframe_write_index_], &v_frame_video_info_, buf, GST_MAP_READ ) ) {
Log::Info("MediaPlayer %s Failed to map the video buffer", id_.c_str());
// get presentation time stamp return false;
position_ = buf->pts;
// set start position (i.e. pts of first frame we got)
if (start_position_ == GST_CLOCK_TIME_NONE)
start_position_ = position_;
// keep update time (i.e. actual FPS of update)
timecount_.tic();
} }
// validate frame format
if( GST_VIDEO_INFO_IS_RGB(&(vframe_[vframe_write_index_]).info) && GST_VIDEO_INFO_N_PLANES(&(vframe_[vframe_write_index_]).info) == 1) {
// validate time
if (isimage_ || vframe_position_[vframe_write_index_] != buf->pts)
{
// calculate actual FPS of update
timecount_.tic();
// got a new RGB frame !
vframe_is_full_[vframe_write_index_] = true;
// get presentation time stamp
vframe_position_[vframe_write_index_] = buf->pts;
// set start position (i.e. pts of first frame we got)
if (start_position_ == GST_CLOCK_TIME_NONE)
start_position_ = vframe_position_[vframe_write_index_];
// dual VFRAME mechanism
vframe_write_index_ = (vframe_write_index_ + 1) % 2;
}
}
// unlock
vframe_lock_[i].unlock();
} }
return true; return true;
@@ -809,34 +846,35 @@ GstFlowReturn MediaPlayer::callback_pull_sample_video (GstElement *bin, MediaPla
{ {
GstFlowReturn ret = GST_FLOW_OK; GstFlowReturn ret = GST_FLOW_OK;
if (m && !m->v_frame_is_full_) { // get last sample (non blocking)
GstSample *sample = nullptr;
g_object_get (bin, "last-sample", &sample, NULL);
// get last sample (non blocking) // if got a valid sample
GstSample *sample = nullptr; if (sample != nullptr) {
g_object_get (bin, "last-sample", &sample, NULL);
// if got a valid sample // get buffer from sample
if (sample != nullptr) { GstBuffer *buf = gst_buffer_ref ( gst_sample_get_buffer (sample) );
// get buffer from sample if (m) {
GstBuffer *buf = gst_buffer_ref ( gst_sample_get_buffer (sample) );
// fill frame from buffer // fill frame from buffer
if ( !m->fill_v_frame(buf, m->isimage_) ) if ( !m->fill_v_frame(buf) )
ret = GST_FLOW_ERROR; ret = GST_FLOW_ERROR;
// free buffer
gst_buffer_unref (buf);
}
else
ret = GST_FLOW_FLUSHING;
// cleanup stack of samples (non blocking)
// NB : overkill as gst_app_sink_set_drop is set to TRUE, but better be safe than leak memory...
while (sample != nullptr) {
gst_sample_unref (sample);
sample = gst_app_sink_try_pull_sample( (GstAppSink * ) bin, 0 );
} }
// free buffer
gst_buffer_unref (buf);
}
else
ret = GST_FLOW_FLUSHING;
// cleanup stack of samples (non blocking)
// NB : possible overkill as gst_app_sink_set_drop is set to TRUE in pipeline
while (sample != nullptr) {
gst_sample_unref (sample);
sample = gst_app_sink_try_pull_sample( (GstAppSink * ) bin, 0 );
} }
return ret; return ret;
@@ -963,14 +1001,14 @@ TimeCounter::TimeCounter() {
void TimeCounter::tic () void TimeCounter::tic ()
{ {
// how long since last time // how long since last time
current_time = gst_util_get_timestamp (); GstClockTime t = gst_util_get_timestamp ();
gint64 dt = current_time - last_time; GstClockTime dt = t - last_time;
// one more frame since last time // one more frame since last time
nbFrames++; nbFrames++;
// calculate instantaneous framerate // calculate instantaneous framerate
// Exponential moving averate with previous framerate to filter jutter (50/50) // Exponential moving averate with previous framerate to filter jitter (50/50)
// The divition of frame/time is done on long integer GstClockTime, counting in microsecond // The divition of frame/time is done on long integer GstClockTime, counting in microsecond
// NB: factor 100 to get 0.01 precision // NB: factor 100 to get 0.01 precision
fps = 0.5f * fps + 0.005f * static_cast<float>( ( 100 * GST_SECOND * nbFrames ) / dt ); fps = 0.5f * fps + 0.005f * static_cast<float>( ( 100 * GST_SECOND * nbFrames ) / dt );
@@ -978,16 +1016,25 @@ void TimeCounter::tic ()
// reset counter every second // reset counter every second
if ( dt >= GST_SECOND) if ( dt >= GST_SECOND)
{ {
last_time = current_time; last_time = t;
nbFrames = 0; nbFrames = 0;
} }
}
GstClockTime TimeCounter::dt ()
{
GstClockTime t = gst_util_get_timestamp ();
GstClockTime dt = t - tic_time;
tic_time = t;
// return the instantaneous delta t
return dt;
} }
void TimeCounter::reset () void TimeCounter::reset ()
{ {
current_time = gst_util_get_timestamp (); last_time = gst_util_get_timestamp ();;
last_time = gst_util_get_timestamp(); tic_time = last_time;
nbFrames = 0; nbFrames = 0;
fps = 0.f; fps = 0.f;
} }

View File

@@ -8,6 +8,7 @@
#include <list> #include <list>
#include <map> #include <map>
#include <utility> #include <utility>
#include <mutex>
#include <gst/gst.h> #include <gst/gst.h>
#include <gst/gl/gl.h> #include <gst/gl/gl.h>
@@ -18,15 +19,17 @@ class Visitor;
#define MAX_PLAY_SPEED 20.0 #define MAX_PLAY_SPEED 20.0
#define MIN_PLAY_SPEED 0.1 #define MIN_PLAY_SPEED 0.1
#define N_VFRAME 2
struct TimeCounter { struct TimeCounter {
GstClockTime current_time;
GstClockTime last_time; GstClockTime last_time;
GstClockTime tic_time;
int nbFrames; int nbFrames;
float fps; float fps;
public: public:
TimeCounter(); TimeCounter();
GstClockTime dt();
void tic(); void tic();
void reset(); void reset();
float frameRate() const; float frameRate() const;
@@ -252,6 +255,7 @@ private:
GstClockTime frame_duration_; GstClockTime frame_duration_;
gdouble rate_; gdouble rate_;
LoopMode loop_; LoopMode loop_;
std::atomic<bool> need_loop_;
TimeCounter timecount_; TimeCounter timecount_;
gdouble framerate_; gdouble framerate_;
GstState desired_state_; GstState desired_state_;
@@ -259,10 +263,18 @@ private:
GstDiscoverer *discoverer_; GstDiscoverer *discoverer_;
std::stringstream discoverer_message_; std::stringstream discoverer_message_;
std::string codec_name_; std::string codec_name_;
GstVideoFrame v_frame_;
GstVideoInfo v_frame_video_info_; GstVideoInfo v_frame_video_info_;
std::atomic<bool> v_frame_is_full_;
std::atomic<bool> need_loop_; // GstVideoFrame v_frame_;
// std::atomic<bool> v_frame_is_full_;
guint vframe_read_index_;
guint vframe_write_index_;
GstVideoFrame vframe_[N_VFRAME];
std::atomic<bool> vframe_is_full_[N_VFRAME];
GstClockTime vframe_position_[N_VFRAME];
std::mutex vframe_lock_[N_VFRAME];
std::mutex vframe_index_lock_;
// for PBO // for PBO
guint pbo_[2]; guint pbo_[2];
@@ -283,7 +295,7 @@ private:
void execute_open(); void execute_open();
void execute_loop_command(); void execute_loop_command();
void execute_seek_command(GstClockTime target = GST_CLOCK_TIME_NONE); void execute_seek_command(GstClockTime target = GST_CLOCK_TIME_NONE);
bool fill_v_frame(GstBuffer *buf, bool ignorepts = false); bool fill_v_frame(GstBuffer *buf);
static GstFlowReturn callback_pull_sample_video (GstElement *bin, MediaPlayer *m); static GstFlowReturn callback_pull_sample_video (GstElement *bin, MediaPlayer *m);
static void callback_end_of_video (GstElement *, MediaPlayer *m); static void callback_end_of_video (GstElement *, MediaPlayer *m);