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);
}
// 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
static int _begin = -1;
if (_begin < 0 || id != s.id())

View File

@@ -9,29 +9,23 @@
#include "Log.h"
#include "GstToolkit.h"
#include "BaseToolkit.h"
#include "Settings.h"
#include "MediaPlayer.h"
#include "MultiFileRecorder.h"
MultiFileRecorder::MultiFileRecorder(const std::string &filename) : filename_(filename),
MultiFileRecorder::MultiFileRecorder() :
fps_(0), width_(0), height_(0), bpp_(3),
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;
// set fps and frame duration
// default fps and frame duration
setFramerate(15);
}
inline void MultiFileRecorder::setFramerate (int fps)
{
fps_ = fps;
frame_duration_ = gst_util_uint64_scale_int (1, GST_SECOND, fps_);
}
MultiFileRecorder::~MultiFileRecorder ()
{
if (src_ != nullptr)
@@ -41,11 +35,18 @@ MultiFileRecorder::~MultiFileRecorder ()
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
@@ -115,6 +116,7 @@ bool MultiFileRecorder::add_image (const std::string &image_filename)
return true;
}
bool MultiFileRecorder::start_record (const std::string &video_filename)
{
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
progress_ = 0.f;
files_.clear();
width_ = 0;
height_ = 0;
bpp_ = 0;
rec->progress_ = 0.f;
rec->width_ = 0;
rec->height_ = 0;
rec->bpp_ = 0;
// input files
if ( list.size() < 1 ) {
if ( rec->files_.size() < 1 ) {
Log::Warning("MultiFileRecorder: No image given.");
return;
return filename;
}
// 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.");
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
float inc_ = 1.f / ( (float) list.size() + 2.f);
float inc_ = 1.f / ( (float) rec->files_.size() + 2.f);
// 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
progress_ += inc_;
rec->progress_ += inc_;
// 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
frame_count_++;
files_.push_back( *file );
rec->frame_count_++;
}
else {
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
int max = 100;
while (!accept_buffer_ && --max > 0)
while (!rec->accept_buffer_ && --max > 0)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// progressing
progress_ += inc_;
rec->progress_ += inc_;
}
// close file properly
if ( end_record() ) {
Log::Info("MultiFileRecorder %d images encoded (%ld s), saved in %s.", frame_count_, timestamp_, filename_.c_str());
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
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

View File

@@ -4,6 +4,8 @@
#include <list>
#include <string>
#include <atomic>
#include <vector>
#include <future>
#include <gst/gst.h>
#include <gst/app/gstappsrc.h>
@@ -12,47 +14,35 @@
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:
MultiFileRecorder(const std::string &filename);
MultiFileRecorder();
virtual ~MultiFileRecorder();
void setFramerate (int fps);
inline int framerate () const { return fps_; }
void setProfile (VideoRecorder::Profile profil);
void setProfile (VideoRecorder::Profile p);
inline VideoRecorder::Profile profile () const { return profile_; }
void assemble (std::list<std::string> list);
inline float progress () const { return progress_; }
inline void setFiles (std::list<std::string> list) { files_ = list; }
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 height () const { return height_; }
inline float progress () const { return progress_; }
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:
// gstreamer functions
static std::string assemble (MultiFileRecorder *rec);
bool start_record (const std::string &video_filename);
bool add_image (const std::string &image_filename);
bool end_record();
@@ -60,6 +50,31 @@ protected:
// gstreamer callbacks
static void callback_need_data (GstAppSrc *, guint, 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

View File

@@ -51,14 +51,6 @@ MultiFileSequence::MultiFileSequence(const std::list<std::string> &list_files)
{
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() ) {
MediaInfo media = MediaPlayer::UriDiscoverer( GstToolkit::filename_to_uri( list_files.front() ) );
if (media.valid && media.isimage) {
@@ -70,6 +62,14 @@ MultiFileSequence::MultiFileSequence(const std::list<std::string> &list_files)
else
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

View File

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

View File

@@ -87,6 +87,7 @@ using namespace std;
#include "NetworkSource.h"
#include "SrtReceiverSource.h"
#include "StreamSource.h"
#include "MultiFileSource.h"
#include "PickingVisitor.h"
#include "ImageFilter.h"
#include "ImageShader.h"
@@ -104,6 +105,7 @@ TextEditor _editor;
#define LABEL_AUTO_MEDIA_PLAYER ICON_FA_CARET_SQUARE_RIGHT " Dynamic selection"
#define LABEL_STORE_SELECTION " Store selection"
#define LABEL_EDIT_FADING ICON_FA_RANDOM " Fade in & out"
#define LABEL_VIDEO_SEQUENCE " Encode an image sequence"
// utility functions
void ShowAboutGStreamer(bool* p_open);
@@ -6219,7 +6221,7 @@ void Navigator::RenderNewPannel()
// News Source selection pannel
//
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_COG " Generated",
ICON_FA_SYNC " Internal"
@@ -6237,7 +6239,7 @@ void Navigator::RenderNewPannel()
static DialogToolkit::OpenFolderDialog folderimportdialog("Select Folder");
// 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();
// Indication
ImGui::SameLine();
@@ -6424,62 +6426,126 @@ void Navigator::RenderNewPannel()
// Folder Source creator
else if (Settings::application.source.new_type == SOURCE_SEQUENCE){
bool update_new_source = false;
static DialogToolkit::MultipleImagesDialog _selectImagesDialog("Select Images");
//static bool _create_video_sequence = false;
static DialogToolkit::MultipleImagesDialog _selectImagesDialog("Select multiple images");
static MultiFileSequence _numbered_sequence;
static MultiFileRecorder _video_recorder;
static int _fps = 25;
// 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();
_selectImagesDialog.open();
}
// Indication
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
if (_selectImagesDialog.closed()) {
// clear
new_source_preview_.setSource();
// store list of files from dialog
sourceSequenceFiles = _selectImagesDialog.images();
if (sourceSequenceFiles.empty())
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
if (sourceSequenceFiles.size() > 1) {
// set framerate
static int _fps = 30;
static bool _fps_changed = false;
ImGui::Text("\nCreate image sequence:");
// 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);
if ( ImGui::SliderInt("Framerate", &_fps, 1, 30, "%d fps") ) {
_fps_changed = true;
}
// only call for new source after mouse release to avoid repeating call to re-open the stream
else if (_fps_changed && ImGui::IsMouseReleased(ImGuiMouseButton_Left)){
update_new_source = true;
_fps_changed = false;
ImGui::InputText("Images", (char *)info.c_str(), info.size(), ImGuiInputTextFlags_ReadOnly);
info.clear();
if (_numbered_sequence.location.empty())
info.append("Not consecutively numbered");
else
info.appendf("%s", SystemToolkit::base_filename(_numbered_sequence.location).c_str());
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) {
std::string label = BaseToolkit::transliterate( BaseToolkit::common_pattern(sourceSequenceFiles) );
new_source_preview_.setSource( Mixer::manager().createSourceMultifile(sourceSequenceFiles, _fps), label);
ImGui::Spacing();
// 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
else if (sourceSequenceFiles.size() > 0) {
ImGui::Text("Single file selected");
if (update_new_source) {
std::string label = BaseToolkit::transliterate( sourceSequenceFiles.front() );
new_source_preview_.setSource( Mixer::manager().createSourceFile(sourceSequenceFiles.front()), label);
}
// open image file as source
std::string label = BaseToolkit::transliterate( sourceSequenceFiles.front() );
new_source_preview_.setSource( Mixer::manager().createSourceFile(sourceSequenceFiles.front()), label);
// done with sequence
sourceSequenceFiles.clear();
}
}
// Internal Source creator
else if (Settings::application.source.new_type == SOURCE_INTERNAL){