diff --git a/ImGuiVisitor.cpp b/ImGuiVisitor.cpp index a19ee80..479a7b5 100644 --- a/ImGuiVisitor.cpp +++ b/ImGuiVisitor.cpp @@ -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()) diff --git a/MultiFileRecorder.cpp b/MultiFileRecorder.cpp index 96a7d61..636438c 100644 --- a/MultiFileRecorder.cpp +++ b/MultiFileRecorder.cpp @@ -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 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 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 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 diff --git a/MultiFileRecorder.h b/MultiFileRecorder.h index e50c50f..4359d98 100644 --- a/MultiFileRecorder.h +++ b/MultiFileRecorder.h @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include @@ -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 endofstream_; - std::atomic accept_buffer_; - - float progress_; - std::list 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 list); - - inline float progress () const { return progress_; } + inline void setFiles (std::list list) { files_ = list; } inline std::list 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 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 files_; + GstElement *pipeline_; + GstAppSrc *src_; + guint64 frame_count_; + GstClockTime timestamp_; + GstClockTime frame_duration_; + std::atomic cancel_; + std::atomic endofstream_; + std::atomic accept_buffer_; + + // progress and result + float progress_; + std::vector< std::future >promises_; }; #endif // MULTIFILERECORDER_H diff --git a/MultiFileSource.cpp b/MultiFileSource.cpp index 5596552..2416676 100644 --- a/MultiFileSource.cpp +++ b/MultiFileSource.cpp @@ -51,14 +51,6 @@ MultiFileSequence::MultiFileSequence(const std::list &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 &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 diff --git a/Settings.h b/Settings.h index fda2d95..db27e0c 100644 --- a/Settings.h +++ b/Settings.h @@ -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; } diff --git a/UserInterfaceManager.cpp b/UserInterfaceManager.cpp index ed9b419..1a60e4d 100644 --- a/UserInterfaceManager.cpp +++ b/UserInterfaceManager.cpp @@ -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){