Files
vimix/MultiFileRecorder.cpp
Bruno Herbelin 34d52c975e Compilation fix
not needed call of non standard gst call
2022-07-05 22:17:59 +02:00

511 lines
16 KiB
C++

#include <thread>
// gstreamer
#include <gst/gstformat.h>
#include <gst/video/video.h>
#include <stb_image.h>
#include "Log.h"
#include "GstToolkit.h"
#include "BaseToolkit.h"
#include "Settings.h"
#include "MediaPlayer.h"
#include "MultiFileRecorder.h"
MultiFileRecorder::MultiFileRecorder() :
fps_(0), width_(0), height_(0), bpp_(3),
pipeline_(nullptr), src_(nullptr), frame_count_(0), timestamp_(0), frame_duration_(0),
cancel_(false), endofstream_(false), accept_buffer_(false), progress_(0.f)
{
// default profile
profile_ = VideoRecorder::H264_STANDARD;
// default fps and frame duration
setFramerate(15);
}
MultiFileRecorder::~MultiFileRecorder ()
{
if (src_ != nullptr)
gst_object_unref (src_);
if (pipeline_ != nullptr)
gst_object_unref (pipeline_);
}
void MultiFileRecorder::setFramerate (int fps)
{
fps_ = fps;
frame_duration_ = gst_util_uint64_scale_int (1, GST_SECOND, fps_);
}
void MultiFileRecorder::setProfile (VideoRecorder::Profile p)
{
if (p < VideoRecorder::VP8)
profile_ = p;
else
profile_ = VideoRecorder::H264_STANDARD;
}
// appsrc needs data and we should start sending
void MultiFileRecorder::callback_need_data (GstAppSrc *, guint , gpointer p)
{
MultiFileRecorder *grabber = static_cast<MultiFileRecorder *>(p);
if (grabber)
grabber->accept_buffer_ = true;
}
// appsrc has enough data and we can stop sending
void MultiFileRecorder::callback_enough_data (GstAppSrc *, gpointer p)
{
MultiFileRecorder *grabber = static_cast<MultiFileRecorder *>(p);
if (grabber)
grabber->accept_buffer_ = false;
}
bool MultiFileRecorder::add_image (const std::string &image_filename)
{
if (image_filename.empty())
return false;
// read pix
int c = 0;
int w = 0;
int h = 0;
unsigned char* rgb = stbi_load(image_filename.c_str(), &w, &h, &c, bpp_);
if ( rgb && w == width_ && h == height_ && c == bpp_) {
// new buffer
guint size = width_ * height_ * bpp_;
GstBuffer *buffer = gst_buffer_new_and_alloc (size);
// map gst buffer into a memory WRITE target
GstMapInfo map;
gst_buffer_map (buffer, &map, GST_MAP_WRITE);
// transfer pixels from memory to buffer memory
memmove(map.data, rgb, size);
// un-map
gst_buffer_unmap (buffer, &map);
// free stbi memory
stbi_image_free( rgb );
//g_print("frame_added @ timestamp = %ld\n", timestamp_);
GST_BUFFER_DTS(buffer) = GST_BUFFER_PTS(buffer) = timestamp_;
// set frame duration
buffer->duration = frame_duration_;
// monotonic time increment to keep fixed FPS
timestamp_ += frame_duration_;
// push frame
if ( gst_app_src_push_buffer (src_, buffer) != GST_FLOW_OK )
return false;
}
else
return false;
return true;
}
bool MultiFileRecorder::start_record (const std::string &video_filename)
{
if ( video_filename.empty() ) {
Log::Warning("MultiFileRecorder Invalid file name");
return false;
}
if (width_ == 0 || height_ == 0 ) {
Log::Warning("MultiFileRecorder Invalid resolution");
return false;
}
// create a gstreamer pipeline
std::string description = "appsrc name=src ! queue ! videoconvert ! ";
description += VideoRecorder::profile_description[ profile_ ];
description += "qtmux ! filesink name=sink";
// parse pipeline descriptor
GError *error = NULL;
pipeline_ = gst_parse_launch (description.c_str(), &error);
if (error != NULL) {
Log::Warning("MultiFileRecorder Could not construct pipeline %s:\n%s", description.c_str(), error->message);
g_clear_error (&error);
return false;
}
// setup file sink
GstElement *sink = gst_bin_get_by_name (GST_BIN (pipeline_), "sink");
if (!sink) {
Log::Warning("MultiFileRecorder Could not configure sink");
return false;
}
g_object_set (G_OBJECT (sink),
"location", video_filename.c_str(),
"sync", FALSE,
NULL);
// setup custom app source
src_ = GST_APP_SRC( gst_bin_get_by_name (GST_BIN (pipeline_), "src") );
if (!src_) {
Log::Warning("MultiFileRecorder : Failed to configure frame grabber.");
return false;
}
g_object_set (G_OBJECT (src_),
"is-live", TRUE,
"format", GST_FORMAT_TIME,
"do-timestamp", FALSE,
NULL);
// configure stream
gst_app_src_set_emit_signals( src_, FALSE);
gst_app_src_set_stream_type ( src_, GST_APP_STREAM_TYPE_STREAM);
gst_app_src_set_latency ( src_, -1, 0);
// Set buffer size
gst_app_src_set_max_bytes( src_, MIN_BUFFER_SIZE);
// specify recorder resolution and framerate in the source caps
GstCaps *caps = gst_caps_new_simple ("video/x-raw",
"format", G_TYPE_STRING, bpp_ < 4 ? "RGB" : "RGBA",
"width", G_TYPE_INT, width_,
"height", G_TYPE_INT, height_,
"framerate", GST_TYPE_FRACTION, fps_, 1,
NULL);
gst_app_src_set_caps (src_, caps );
gst_caps_unref (caps);
GstAppSrcCallbacks callbacks;
callbacks.need_data = MultiFileRecorder::callback_need_data;
callbacks.enough_data = MultiFileRecorder::callback_enough_data;
callbacks.seek_data = NULL; // stream type is not seekable
gst_app_src_set_callbacks (src_, &callbacks, this, NULL);
// start recording
GstStateChangeReturn ret = gst_element_set_state (pipeline_, GST_STATE_PLAYING);
if (ret == GST_STATE_CHANGE_FAILURE) {
Log::Warning("MultiFileRecorder: Failed to start frame grabber.");
return false;
}
// wait ready
int max = 100;
accept_buffer_ = false;
while (!accept_buffer_ && --max > 0)
std::this_thread::sleep_for(std::chrono::milliseconds(5));
// // send request key frame upstream
// GstEvent* event = gst_video_event_new_upstream_force_key_unit (GST_CLOCK_TIME_NONE, TRUE, 1);
// if (!gst_element_send_event( GST_ELEMENT(sink), event) )
// Log::Warning("MultiFileRecorder: Failed to request key unit.");
// GstPad *padsrc = gst_element_get_static_pad ( GST_ELEMENT (sink), "sink");
// gst_pad_push_event(padsrc, gst_event_new_custom(GST_EVENT_CUSTOM_UPSTREAM, gst_structure_new("GstForceKeyUnit", "all-headers",
// G_TYPE_BOOLEAN, TRUE, NULL)));
// gst_object_unref (padsrc);
// // send request key frame downstream
// GstPad *padsrc = gst_element_get_static_pad ( GST_ELEMENT (src_), "src");
// GstStructure *s = gst_structure_new("GstForceKeyUnit",
// "timestamp", G_TYPE_UINT64, 0,
// "stream-time", G_TYPE_UINT64, 0,
// "running-time", G_TYPE_UINT64, 0,
// "all-headers", G_TYPE_BOOLEAN, TRUE,
// NULL);
// if ( !gst_pad_push_event(padsrc, gst_event_new_custom(GST_EVENT_CUSTOM_DOWNSTREAM, s )) )
// Log::Warning("MultiFileRecorder: Failed to force key unit.");
// gst_object_unref (padsrc);
GstEvent* event = gst_video_event_new_downstream_force_key_unit (GST_CLOCK_TIME_NONE, GST_CLOCK_TIME_NONE, GST_CLOCK_TIME_NONE, TRUE, 1);
if (!gst_element_send_event( GST_ELEMENT(src_), event) )
Log::Warning("MultiFileRecorder: Failed to force key unit.");
return true;
}
bool MultiFileRecorder::end_record ()
{
bool ret = true;
GstBus *bus = gst_pipeline_get_bus (GST_PIPELINE (pipeline_));
// ensure duration is set to correct
// gst_app_src_set_duration (src_, frame_count_ * frame_duration_ );
gst_app_src_set_duration (src_, timestamp_);
// send end of stream
gst_app_src_end_of_stream (src_);
// wait to receive end of stream
GstMessage *msg = gst_bus_timed_pop_filtered(bus, GST_SECOND, GST_MESSAGE_EOS);
if ( msg == NULL ) {
Log::Warning("MultiFileRecorder: Failed to close recording.");
ret = false;
}
// stop the pipeline
GstStateChangeReturn r = gst_element_set_state (pipeline_, GST_STATE_NULL);
if (r == GST_STATE_CHANGE_ASYNC) {
gst_element_get_state (pipeline_, NULL, NULL, GST_SECOND);
g_print("... ASYNC END !\n");
}
else if (r == GST_STATE_CHANGE_FAILURE) {
Log::Warning("MultiFileRecorder: Failed to end recording.");
ret = false;
}
// clean
gst_object_unref (src_);
src_ = nullptr;
gst_object_unref (pipeline_);
pipeline_ = nullptr;
return ret;
}
void MultiFileRecorder::start ()
{
if ( promises_.empty() ) {
filename_ = std::string();
promises_.emplace_back( std::async(std::launch::async, assemble, this) );
}
}
void MultiFileRecorder::cancel ()
{
cancel_ = true;
}
bool MultiFileRecorder::finished ()
{
if ( !promises_.empty() ) {
// check that file dialog thread finished
if (promises_.back().wait_for(std::chrono::milliseconds(4)) == std::future_status::ready ) {
// get the filename from encoder
filename_ = promises_.back().get();
if (!filename_.empty()) {
// save path location
Settings::application.recentRecordings.push(filename_);
}
// done with this recoding
promises_.pop_back();
return true;
}
}
return false;
}
std::string MultiFileRecorder::assemble (MultiFileRecorder *rec)
{
std::string filename;
// reset
rec->progress_ = 0.f;
rec->width_ = 0;
rec->height_ = 0;
rec->bpp_ = 0;
// input files
if ( rec->files_.size() < 1 ) {
Log::Warning("MultiFileRecorder: No image given.");
return filename;
}
// set recorder resolution from first image
stbi_info( rec->files_.front().c_str(), &rec->width_, &rec->height_, &rec->bpp_);
if ( rec->width_ < 10 || rec->height_ < 10 || rec->bpp_ < 3 ) {
Log::Warning("MultiFileRecorder: invalid image.");
return filename;
}
Log::Info("MultiFileRecorder creating video %d x %d : %d.", rec->width_, rec->height_, rec->bpp_);
// progress increment
float inc_ = 1.f / ( (float) rec->files_.size() + 2.f);
// initialize
rec->frame_count_ = 0;
filename = BaseToolkit::common_prefix (rec->files_);
filename += "_sequence.mov";
if ( rec->start_record( filename ) )
{
// progressing
rec->progress_ += inc_;
// loop over images to open
for (auto file = rec->files_.cbegin(); file != rec->files_.cend(); ++file) {
if ( rec->cancel_ )
break;
if ( rec->add_image( *file ) ) {
// validate file
rec->frame_count_++;
}
else {
Log::Info("MultiFileRecorder could not include images %s.", file->c_str());
}
// pause in case appsrc buffer is full
int max = 100;
while (!rec->accept_buffer_ && --max > 0)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// progressing
rec->progress_ += inc_;
}
// close file properly
if ( rec->end_record() ) {
Log::Info("MultiFileRecorder %d images encoded (%ld s), saved in %s.", rec->frame_count_, rec->timestamp_, filename.c_str());
}
}
else
filename = std::string();
// finished
rec->progress_ = 1.f;
return filename;
}
// alternative https://gstreamer.freedesktop.org/documentation/application-development/advanced/pipeline-manipulation.html?gi-language=c#changing-elements-in-a-pipeline
// // initialize recorder with first image
// std::string uri = GstToolkit::filename_to_uri( list.front() );
// MediaInfo media = MediaPlayer::UriDiscoverer( uri );
// // if media is valid image
// if (!media.valid || !media.isimage || media.width < 1 || media.height < 1) {
// Log::Warning("MultiFileRecorder: Invalid image.");
// return;
// }
// // set recorder resolution from first image
// width_ = media.width;
// height_ = media.height;
//GstFlowReturn MultiFileRecorder::callback_new_preroll (GstAppSink *sink, gpointer p)
//{
// GstFlowReturn ret = GST_FLOW_OK;
// // blocking read pre-roll samples
// GstSample *sample = gst_app_sink_pull_preroll(sink);
// // if got a valid sample
// if (sample != NULL) {
// MultiFileRecorder *grabber = static_cast<MultiFileRecorder *>(p);
// if (grabber) {
// GstBuffer *buf = gst_sample_get_buffer (sample);
// // store frame to recorder
// grabber->addFrame(buf);
// }
// }
// else
// ret = GST_FLOW_FLUSHING;
// // release sample
// gst_sample_unref (sample);
// return ret;
//}
//bool MultiFileRecorder::get_frame_from_image (const std::string &file)
//{
// std::string description = "uridecodebin name=decoder ! queue ! videoconvert ! appsink name=sink";
// // parse pipeline descriptor
// GError *error = NULL;
// GstElement *pipeline = gst_parse_launch (description.c_str(), &error);
// if (error != NULL) {
// Log::Warning("MultiFileRecorder Could not construct pipeline %s:\n%s", description.c_str(), error->message);
// g_clear_error (&error);
// return false;
// }
// // setup decoder
// GstElement *dec = gst_bin_get_by_name (GST_BIN (pipeline), "decoder");
// if (!dec) {
// Log::Warning("MultiFileRecorder Could not configure decoder");
// return false;
// }
// g_object_set (dec, "uri", GstToolkit::filename_to_uri( file ).c_str(), NULL);
// // setup appsink
// GstElement *sink = gst_bin_get_by_name (GST_BIN (pipeline), "sink");
// if (!sink) {
// Log::Warning("MultiFileRecorder Could not configure sink");
// return false;
// }
// // instruct the sink to send samples synched in time
// gst_base_sink_set_sync (GST_BASE_SINK(sink), true);
// // instruct sink to use the required caps
// GstCaps *caps = gst_caps_new_simple ("video/x-raw",
// "format", G_TYPE_STRING, "RGB",
// "width", G_TYPE_INT, width_,
// "height", G_TYPE_INT, height_,
// NULL);
// gst_app_sink_set_caps (GST_APP_SINK(sink), caps);
// // set the callback to receive image preroll
// GstAppSinkCallbacks callbacks;
// callbacks.eos = NULL;
// callbacks.new_sample = NULL;
//#if GST_VERSION_MINOR > 18 && GST_VERSION_MAJOR > 0
// callbacks.new_event = NULL;
//#endif
// callbacks.new_preroll = MultiFileRecorder::callback_new_preroll;
// gst_app_sink_set_callbacks (GST_APP_SINK(sink), &callbacks, this, NULL);
// gst_app_sink_set_emit_signals (GST_APP_SINK(sink), false);
// // done with refs
// gst_object_unref (sink);
// gst_object_unref (dec);
// gst_caps_unref (caps);
// // set to PAUSE to trigger preroll
// GstStateChangeReturn ret = gst_element_set_state (pipeline, GST_STATE_PAUSED);
// if (ret == GST_STATE_CHANGE_FAILURE) {
// Log::Warning("MultiFileRecorder Could not open '%s'", file.c_str());
// return false;
// }
// // wait to accept buffer
// int max = 100;
// frame_added_ = false;
// while (!frame_added_ && --max > 0)
// std::this_thread::sleep_for(std::chrono::milliseconds(10));
// GstState state = GST_STATE_NULL;
// gst_element_set_state (pipeline_, state);
// gst_element_get_state (pipeline_, &state, NULL, GST_CLOCK_TIME_NONE);
// g_print("pipeline stoped\n");
// // end pipeline
// gst_object_unref (pipeline);
// return true;
//}