Integration of MultiFileRecorder in UI for sequence creation

This commit is contained in:
Bruno Herbelin
2022-07-04 00:07:23 +02:00
parent af009e03a0
commit 85194c7f4f
6 changed files with 235 additions and 104 deletions

View File

@@ -1155,6 +1155,14 @@ void ImGuiVisitor::visit (MultiFileSource& s)
ImGui::SetCursorPos(pos); ImGui::SetCursorPos(pos);
} }
// Filename pattern
ImGuiTextBuffer info;
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.14f, 0.14f, 0.14f, 0.9f));
info.appendf("%s", SystemToolkit::base_filename(s.sequence().location).c_str());
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
ImGui::InputText("Filenames", (char *)info.c_str(), info.size(), ImGuiInputTextFlags_ReadOnly);
ImGui::PopStyleColor(1);
// change range // change range
static int _begin = -1; static int _begin = -1;
if (_begin < 0 || id != s.id()) if (_begin < 0 || id != s.id())

View File

@@ -9,29 +9,23 @@
#include "Log.h" #include "Log.h"
#include "GstToolkit.h" #include "GstToolkit.h"
#include "BaseToolkit.h" #include "BaseToolkit.h"
#include "Settings.h"
#include "MediaPlayer.h" #include "MediaPlayer.h"
#include "MultiFileRecorder.h" #include "MultiFileRecorder.h"
MultiFileRecorder::MultiFileRecorder(const std::string &filename) : filename_(filename), MultiFileRecorder::MultiFileRecorder() :
fps_(0), width_(0), height_(0), bpp_(3), fps_(0), width_(0), height_(0), bpp_(3),
pipeline_(nullptr), src_(nullptr), frame_count_(0), timestamp_(0), frame_duration_(0), pipeline_(nullptr), src_(nullptr), frame_count_(0), timestamp_(0), frame_duration_(0),
endofstream_(false), accept_buffer_(false), progress_(0.f) cancel_(false), endofstream_(false), accept_buffer_(false), progress_(0.f)
{ {
// default // default profile
profile_ = VideoRecorder::H264_STANDARD; profile_ = VideoRecorder::H264_STANDARD;
// set fps and frame duration // default fps and frame duration
setFramerate(15); setFramerate(15);
} }
inline void MultiFileRecorder::setFramerate (int fps)
{
fps_ = fps;
frame_duration_ = gst_util_uint64_scale_int (1, GST_SECOND, fps_);
}
MultiFileRecorder::~MultiFileRecorder () MultiFileRecorder::~MultiFileRecorder ()
{ {
if (src_ != nullptr) if (src_ != nullptr)
@@ -41,11 +35,18 @@ MultiFileRecorder::~MultiFileRecorder ()
gst_object_unref (pipeline_); gst_object_unref (pipeline_);
} }
void MultiFileRecorder::assembleImages(std::list<std::string> list, const std::string &filename) void MultiFileRecorder::setFramerate (int fps)
{ {
MultiFileRecorder recorder( filename ); fps_ = fps;
frame_duration_ = gst_util_uint64_scale_int (1, GST_SECOND, fps_);
}
recorder.assemble( list ); 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 // appsrc needs data and we should start sending
@@ -115,6 +116,7 @@ bool MultiFileRecorder::add_image (const std::string &image_filename)
return true; return true;
} }
bool MultiFileRecorder::start_record (const std::string &video_filename) bool MultiFileRecorder::start_record (const std::string &video_filename)
{ {
if ( video_filename.empty() ) { if ( video_filename.empty() ) {
@@ -276,50 +278,87 @@ bool MultiFileRecorder::end_record ()
void MultiFileRecorder::assemble (std::list<std::string> list) 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 // reset
progress_ = 0.f; rec->progress_ = 0.f;
files_.clear(); rec->width_ = 0;
width_ = 0; rec->height_ = 0;
height_ = 0; rec->bpp_ = 0;
bpp_ = 0;
// input files // input files
if ( list.size() < 1 ) { if ( rec->files_.size() < 1 ) {
Log::Warning("MultiFileRecorder: No image given."); Log::Warning("MultiFileRecorder: No image given.");
return; return filename;
} }
// set recorder resolution from first image // set recorder resolution from first image
stbi_info( list.front().c_str(), &width_, &height_, &bpp_); stbi_info( rec->files_.front().c_str(), &rec->width_, &rec->height_, &rec->bpp_);
if ( width_ < 10 || height_ < 10 || bpp_ < 3 ) { if ( rec->width_ < 10 || rec->height_ < 10 || rec->bpp_ < 3 ) {
Log::Warning("MultiFileRecorder: invalid image."); Log::Warning("MultiFileRecorder: invalid image.");
return; return filename;
} }
Log::Info("MultiFileRecorder creating video %d x %d : %d.", width_, height_, bpp_);
Log::Info("MultiFileRecorder creating video %d x %d : %d.", rec->width_, rec->height_, rec->bpp_);
// progress increment // progress increment
float inc_ = 1.f / ( (float) list.size() + 2.f); float inc_ = 1.f / ( (float) rec->files_.size() + 2.f);
// initialize // initialize
frame_count_ = 0; rec->frame_count_ = 0;
filename = BaseToolkit::common_prefix (rec->files_);
filename += "_sequence.mov";
if ( start_record( filename_ ) ) if ( rec->start_record( filename ) )
{ {
// progressing // progressing
progress_ += inc_; rec->progress_ += inc_;
// loop over images to open // loop over images to open
for (auto file = list.cbegin(); file != list.cend(); ++file) { for (auto file = rec->files_.cbegin(); file != rec->files_.cend(); ++file) {
if ( add_image( *file ) ) { if ( rec->cancel_ )
break;
if ( rec->add_image( *file ) ) {
// validate file // validate file
frame_count_++; rec->frame_count_++;
files_.push_back( *file );
} }
else { else {
Log::Info("MultiFileRecorder could not include images %s.", file->c_str()); Log::Info("MultiFileRecorder could not include images %s.", file->c_str());
@@ -327,22 +366,25 @@ void MultiFileRecorder::assemble (std::list<std::string> list)
// pause in case appsrc buffer is full // pause in case appsrc buffer is full
int max = 100; int max = 100;
while (!accept_buffer_ && --max > 0) while (!rec->accept_buffer_ && --max > 0)
std::this_thread::sleep_for(std::chrono::milliseconds(10)); std::this_thread::sleep_for(std::chrono::milliseconds(10));
// progressing // progressing
progress_ += inc_; rec->progress_ += inc_;
} }
// close file properly // close file properly
if ( end_record() ) { if ( rec->end_record() ) {
Log::Info("MultiFileRecorder %d images encoded (%ld s), saved in %s.", frame_count_, timestamp_, filename_.c_str()); Log::Info("MultiFileRecorder %d images encoded (%ld s), saved in %s.", rec->frame_count_, rec->timestamp_, filename.c_str());
} }
} }
else
filename = std::string();
// finished // finished
progress_ = 1.f; 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 // alternative https://gstreamer.freedesktop.org/documentation/application-development/advanced/pipeline-manipulation.html?gi-language=c#changing-elements-in-a-pipeline

View File

@@ -4,6 +4,8 @@
#include <list> #include <list>
#include <string> #include <string>
#include <atomic> #include <atomic>
#include <vector>
#include <future>
#include <gst/gst.h> #include <gst/gst.h>
#include <gst/app/gstappsrc.h> #include <gst/app/gstappsrc.h>
@@ -12,47 +14,35 @@
class MultiFileRecorder class MultiFileRecorder
{ {
std::string filename_;
VideoRecorder::Profile profile_;
int fps_;
int width_;
int height_;
int bpp_;
GstElement *pipeline_;
GstAppSrc *src_;
guint64 frame_count_;
GstClockTime timestamp_;
GstClockTime frame_duration_;
std::atomic<bool> endofstream_;
std::atomic<bool> accept_buffer_;
float progress_;
std::list<std::string> files_;
public: public:
MultiFileRecorder(const std::string &filename); MultiFileRecorder();
virtual ~MultiFileRecorder(); virtual ~MultiFileRecorder();
void setFramerate (int fps); void setFramerate (int fps);
inline int framerate () const { return fps_; } inline int framerate () const { return fps_; }
void setProfile (VideoRecorder::Profile profil); void setProfile (VideoRecorder::Profile p);
inline VideoRecorder::Profile profile () const { return profile_; } inline VideoRecorder::Profile profile () const { return profile_; }
void assemble (std::list<std::string> list); inline void setFiles (std::list<std::string> list) { files_ = list; }
inline float progress () const { return progress_; }
inline std::list<std::string> files () const { return files_; } inline std::list<std::string> files () const { return files_; }
// process control
void start ();
void cancel ();
bool finished ();
// result
inline std::string filename () const { return filename_; }
inline int width () const { return width_; } inline int width () const { return width_; }
inline int height () const { return height_; } inline int height () const { return height_; }
inline float progress () const { return progress_; }
inline guint64 numFrames () const { return frame_count_; } inline guint64 numFrames () const { return frame_count_; }
// std::thread(MultiFileRecorder::assembleImages, sourceSequenceFiles, "/home/bhbn/test.mov").detach();
static void assembleImages(std::list<std::string> list, const std::string &filename);
protected: protected:
// gstreamer functions // gstreamer functions
static std::string assemble (MultiFileRecorder *rec);
bool start_record (const std::string &video_filename); bool start_record (const std::string &video_filename);
bool add_image (const std::string &image_filename); bool add_image (const std::string &image_filename);
bool end_record(); bool end_record();
@@ -60,6 +50,31 @@ protected:
// gstreamer callbacks // gstreamer callbacks
static void callback_need_data (GstAppSrc *, guint, gpointer user_data); static void callback_need_data (GstAppSrc *, guint, gpointer user_data);
static void callback_enough_data (GstAppSrc *, gpointer user_data); static void callback_enough_data (GstAppSrc *, gpointer user_data);
private:
// video properties
std::string filename_;
VideoRecorder::Profile profile_;
int fps_;
int width_;
int height_;
int bpp_;
// encoder
std::list<std::string> files_;
GstElement *pipeline_;
GstAppSrc *src_;
guint64 frame_count_;
GstClockTime timestamp_;
GstClockTime frame_duration_;
std::atomic<bool> cancel_;
std::atomic<bool> endofstream_;
std::atomic<bool> accept_buffer_;
// progress and result
float progress_;
std::vector< std::future<std::string> >promises_;
}; };
#endif // MULTIFILERECORDER_H #endif // MULTIFILERECORDER_H

View File

@@ -51,14 +51,6 @@ MultiFileSequence::MultiFileSequence(const std::list<std::string> &list_files)
{ {
location = BaseToolkit::common_numbered_pattern(list_files, &min, &max); location = BaseToolkit::common_numbered_pattern(list_files, &min, &max);
// sanity check: the location pattern looks like a filename and seems consecutive numbered
if ( SystemToolkit::extension_filename(location).empty() ||
SystemToolkit::path_filename(location) != SystemToolkit::path_filename(list_files.front()) ||
list_files.size() != (size_t) (max - min) + 1 ) {
Log::Info("MultiFileSequence '%s' invalid.", location.c_str());
location.clear();
}
if ( !location.empty() ) { if ( !location.empty() ) {
MediaInfo media = MediaPlayer::UriDiscoverer( GstToolkit::filename_to_uri( list_files.front() ) ); MediaInfo media = MediaPlayer::UriDiscoverer( GstToolkit::filename_to_uri( list_files.front() ) );
if (media.valid && media.isimage) { if (media.valid && media.isimage) {
@@ -70,6 +62,14 @@ MultiFileSequence::MultiFileSequence(const std::list<std::string> &list_files)
else else
Log::Info("MultiFileSequence '%s' does not list images.", location.c_str()); Log::Info("MultiFileSequence '%s' does not list images.", location.c_str());
} }
// sanity check: the location pattern looks like a filename and seems consecutive numbered
if ( SystemToolkit::extension_filename(location).empty() ||
SystemToolkit::path_filename(location) != SystemToolkit::path_filename(list_files.front()) ||
list_files.size() != (size_t) (max - min) + 1 ) {
Log::Info("MultiFileSequence '%s' invalid.", location.c_str());
location.clear();
}
} }
bool MultiFileSequence::valid() const bool MultiFileSequence::valid() const

View File

@@ -115,7 +115,7 @@ struct History
History() { History() {
path = IMGUI_LABEL_RECENT_FILES; path = IMGUI_LABEL_RECENT_FILES;
front_is_valid = false; front_is_valid = false;
load_at_start = false; load_at_start = true;
save_on_exit = true; save_on_exit = true;
changed = false; changed = false;
} }

View File

@@ -87,6 +87,7 @@ using namespace std;
#include "NetworkSource.h" #include "NetworkSource.h"
#include "SrtReceiverSource.h" #include "SrtReceiverSource.h"
#include "StreamSource.h" #include "StreamSource.h"
#include "MultiFileSource.h"
#include "PickingVisitor.h" #include "PickingVisitor.h"
#include "ImageFilter.h" #include "ImageFilter.h"
#include "ImageShader.h" #include "ImageShader.h"
@@ -104,6 +105,7 @@ TextEditor _editor;
#define LABEL_AUTO_MEDIA_PLAYER ICON_FA_CARET_SQUARE_RIGHT " Dynamic selection" #define LABEL_AUTO_MEDIA_PLAYER ICON_FA_CARET_SQUARE_RIGHT " Dynamic selection"
#define LABEL_STORE_SELECTION " Store selection" #define LABEL_STORE_SELECTION " Store selection"
#define LABEL_EDIT_FADING ICON_FA_RANDOM " Fade in & out" #define LABEL_EDIT_FADING ICON_FA_RANDOM " Fade in & out"
#define LABEL_VIDEO_SEQUENCE " Encode an image sequence"
// utility functions // utility functions
void ShowAboutGStreamer(bool* p_open); void ShowAboutGStreamer(bool* p_open);
@@ -6219,7 +6221,7 @@ void Navigator::RenderNewPannel()
// News Source selection pannel // News Source selection pannel
// //
static const char* origin_names[SOURCE_TYPES] = { ICON_FA_PHOTO_VIDEO " File", static const char* origin_names[SOURCE_TYPES] = { ICON_FA_PHOTO_VIDEO " File",
ICON_FA_SORT_NUMERIC_DOWN " Sequence", ICON_FA_IMAGES " Sequence",
ICON_FA_PLUG " Connected", ICON_FA_PLUG " Connected",
ICON_FA_COG " Generated", ICON_FA_COG " Generated",
ICON_FA_SYNC " Internal" ICON_FA_SYNC " Internal"
@@ -6237,7 +6239,7 @@ void Navigator::RenderNewPannel()
static DialogToolkit::OpenFolderDialog folderimportdialog("Select Folder"); static DialogToolkit::OpenFolderDialog folderimportdialog("Select Folder");
// clic button to load file // clic button to load file
if ( ImGui::Button( ICON_FA_FILE_EXPORT " Open File", ImVec2(ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN, 0)) ) if ( ImGui::Button( ICON_FA_FOLDER_OPEN " Open File", ImVec2(ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN, 0)) )
fileimportdialog.open(); fileimportdialog.open();
// Indication // Indication
ImGui::SameLine(); ImGui::SameLine();
@@ -6424,62 +6426,126 @@ void Navigator::RenderNewPannel()
// Folder Source creator // Folder Source creator
else if (Settings::application.source.new_type == SOURCE_SEQUENCE){ else if (Settings::application.source.new_type == SOURCE_SEQUENCE){
bool update_new_source = false; static DialogToolkit::MultipleImagesDialog _selectImagesDialog("Select multiple images");
static DialogToolkit::MultipleImagesDialog _selectImagesDialog("Select Images"); static MultiFileSequence _numbered_sequence;
//static bool _create_video_sequence = false; static MultiFileRecorder _video_recorder;
static int _fps = 25;
// clic button to load file // clic button to load file
if ( ImGui::Button( ICON_FA_IMAGES " Open images", ImVec2(ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN, 0)) ) { if ( ImGui::Button( ICON_FA_FOLDER_OPEN " Open images", ImVec2(ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN, 0)) ) {
sourceSequenceFiles.clear(); sourceSequenceFiles.clear();
_selectImagesDialog.open(); _selectImagesDialog.open();
} }
// Indication // Indication
ImGui::SameLine(); ImGui::SameLine();
ImGuiToolkit::HelpToolTip("Create a source from a sequence of numbered images."); ImGuiToolkit::HelpToolTip("Create a source displaying a sequence of images (PNG, JPG, TIF);\n"
ICON_FA_CARET_RIGHT " files numbered consecutively\n"
ICON_FA_CARET_RIGHT " create a video from many images\n");
// return from thread for folder openning // return from thread for folder openning
if (_selectImagesDialog.closed()) { if (_selectImagesDialog.closed()) {
// clear
new_source_preview_.setSource();
// store list of files from dialog
sourceSequenceFiles = _selectImagesDialog.images(); sourceSequenceFiles = _selectImagesDialog.images();
if (sourceSequenceFiles.empty()) if (sourceSequenceFiles.empty())
Log::Notify("No file selected."); Log::Notify("No file selected.");
// ask to reload the preview
update_new_source = true; // set sequence
_numbered_sequence = MultiFileSequence(sourceSequenceFiles);
// automatically create a MultiFile Source if possible
if (_numbered_sequence.valid()) {
std::string label = BaseToolkit::transliterate( BaseToolkit::common_pattern(sourceSequenceFiles) );
new_source_preview_.setSource( Mixer::manager().createSourceMultifile(sourceSequenceFiles, _fps), label);
}
} }
// multiple files selected // multiple files selected
if (sourceSequenceFiles.size() > 1) { if (sourceSequenceFiles.size() > 1) {
// set framerate ImGui::Text("\nCreate image sequence:");
static int _fps = 30;
static bool _fps_changed = false; // show info sequence
ImGuiTextBuffer info;
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.14f, 0.14f, 0.14f, 0.9f));
info.appendf("%d %s", (int) sourceSequenceFiles.size(), _numbered_sequence.codec.c_str());
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
if ( ImGui::SliderInt("Framerate", &_fps, 1, 30, "%d fps") ) { ImGui::InputText("Images", (char *)info.c_str(), info.size(), ImGuiInputTextFlags_ReadOnly);
_fps_changed = true; info.clear();
} if (_numbered_sequence.location.empty())
// only call for new source after mouse release to avoid repeating call to re-open the stream info.append("Not consecutively numbered");
else if (_fps_changed && ImGui::IsMouseReleased(ImGuiMouseButton_Left)){ else
update_new_source = true; info.appendf("%s", SystemToolkit::base_filename(_numbered_sequence.location).c_str());
_fps_changed = false; ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
ImGui::InputText("Filenames", (char *)info.c_str(), info.size(), ImGuiInputTextFlags_ReadOnly);
ImGui::PopStyleColor(1);
// offer to open file browser at location
std::string path = SystemToolkit::path_filename(sourceSequenceFiles.front());
std::string label = BaseToolkit::truncated(path, 25);
label = BaseToolkit::transliterate(label);
ImGuiToolkit::ButtonOpenUrl( label.c_str(), path.c_str(), ImVec2(IMGUI_RIGHT_ALIGN, 0) );
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::Text("Folder");
// set framerate
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
ImGui::SliderInt("Framerate", &_fps, 1, 30, "%d fps");
if (ImGui::IsItemDeactivatedAfterEdit()){
if (new_source_preview_.filled()) {
std::string label = BaseToolkit::transliterate( BaseToolkit::common_pattern(sourceSequenceFiles) );
new_source_preview_.setSource( Mixer::manager().createSourceMultifile(sourceSequenceFiles, _fps), label);
}
} }
if (update_new_source) { ImGui::Spacing();
std::string label = BaseToolkit::transliterate( BaseToolkit::common_pattern(sourceSequenceFiles) );
new_source_preview_.setSource( Mixer::manager().createSourceMultifile(sourceSequenceFiles, _fps), label); // Offer to create video from sequence
if ( ImGui::Button( ICON_FA_FILM " Make a video", ImVec2(ImGui::GetContentRegionAvail().x, 0)) ) {
// start video recorder
_video_recorder.setFiles( sourceSequenceFiles );
_video_recorder.setFramerate( _fps );
_video_recorder.setProfile( (VideoRecorder::Profile) Settings::application.record.profile );
_video_recorder.start();
// dialog
ImGui::OpenPopup(LABEL_VIDEO_SEQUENCE);
} }
// video recorder finished: inform and open pannel to import video source from recent recordings
if ( _video_recorder.finished() ) {
Log::Notify("Image sequence saved to %s.", _video_recorder.filename().c_str());
if (Settings::application.recentRecordings.load_at_start)
UserInterface::manager().navigator.setNewMedia(Navigator::MEDIA_RECORDING, _video_recorder.filename());
}
else if (ImGui::BeginPopupModal(LABEL_VIDEO_SEQUENCE, NULL, ImGuiWindowFlags_NoResize))
{
ImGui::Spacing();
ImGui::Text("Please wait while the video is being encoded...");
ImGui::Spacing();
ImGui::ProgressBar(_video_recorder.progress());
if (ImGui::Button("Cancel"))
_video_recorder.cancel();
ImGui::EndPopup();
}
} }
// single file selected // single file selected
else if (sourceSequenceFiles.size() > 0) { else if (sourceSequenceFiles.size() > 0) {
// open image file as source
ImGui::Text("Single file selected"); std::string label = BaseToolkit::transliterate( sourceSequenceFiles.front() );
new_source_preview_.setSource( Mixer::manager().createSourceFile(sourceSequenceFiles.front()), label);
if (update_new_source) { // done with sequence
std::string label = BaseToolkit::transliterate( sourceSequenceFiles.front() ); sourceSequenceFiles.clear();
new_source_preview_.setSource( Mixer::manager().createSourceFile(sourceSequenceFiles.front()), label);
}
} }
} }
// Internal Source creator // Internal Source creator
else if (Settings::application.source.new_type == SOURCE_INTERNAL){ else if (Settings::application.source.new_type == SOURCE_INTERNAL){