From a46e68f1451b7d3ed9ef05cf516c246127185a34 Mon Sep 17 00:00:00 2001 From: Bruno Herbelin Date: Wed, 1 May 2024 23:29:08 +0200 Subject: [PATCH] BugFix Video recorder and image sequence encoder Improved and simplified UI control of encoding of image sequences into video files. Bugs fixed to prevent problems with video recorder. --- src/MultiFileRecorder.cpp | 33 ++++++---- src/Recorder.cpp | 18 ++++-- src/Settings.cpp | 15 ++++- src/Settings.h | 4 +- src/UserInterfaceManager.cpp | 115 ++++++++++++++++++++++++----------- 5 files changed, 131 insertions(+), 54 deletions(-) diff --git a/src/MultiFileRecorder.cpp b/src/MultiFileRecorder.cpp index 9da8a39..96769c1 100644 --- a/src/MultiFileRecorder.cpp +++ b/src/MultiFileRecorder.cpp @@ -79,13 +79,9 @@ bool MultiFileRecorder::add_image (const std::string &image_filename, GstCaps *c // set flag to only read VIDEO g_object_set(G_OBJECT(img_pipeline), "flags", 0x00000001, NULL); - // instruct sink to use the required caps (without framerate) - GstCaps *sinkcaps = gst_caps_copy(caps); - GValue v = {GST_TYPE_FRACTION, {{0}, {1}}}; - gst_caps_set_value(sinkcaps, "framerate", &v); - + // instruct sink to use the required caps GstElement *sink = gst_element_factory_make("appsink", "imgsink"); - gst_app_sink_set_caps(GST_APP_SINK(sink), sinkcaps); + gst_app_sink_set_caps(GST_APP_SINK(sink), caps); // set playbin sink g_object_set(G_OBJECT(img_pipeline), "video-sink", sink, NULL); @@ -137,7 +133,6 @@ bool MultiFileRecorder::add_image (const std::string &image_filename, GstCaps *c } /* Clean up */ - gst_caps_unref(caps); gst_sample_unref(sample); gst_element_set_state(img_pipeline, GST_STATE_NULL); gst_object_unref(GST_OBJECT(img_pipeline)); @@ -165,7 +160,7 @@ bool MultiFileRecorder::start_record (const std::string &video_filename) if (Settings::application.render.gpu_decoding && (int) VideoRecorder::hardware_encoder.size() > 0 && GstToolkit::has_feature(VideoRecorder::hardware_encoder[profile_]) ) { - description += VideoRecorder::hardware_profile_description[Settings::application.record.profile]; + description += VideoRecorder::hardware_profile_description[Settings::application.image_sequence.profile]; Log::Info("MultiFileRecorder use hardware accelerated encoder (%s)", VideoRecorder::hardware_encoder[profile_].c_str()); } // revert to software encoder @@ -317,8 +312,13 @@ bool MultiFileRecorder::finished () // get the filename from encoder filename_ = promises_.back().get(); if (!filename_.empty()) { - // save path location - Settings::application.recentRecordings.push(filename_); + // save path location if valid + std::string uri = GstToolkit::filename_to_uri(filename_); + MediaInfo media = MediaPlayer::UriDiscoverer(uri); + if (media.valid && !media.isimage) + Settings::application.recentRecordings.push(filename_); + else + Settings::application.recentRecordings.remove(filename_); } // done with this recoding promises_.pop_back(); @@ -336,6 +336,7 @@ std::string MultiFileRecorder::assemble (MultiFileRecorder *rec) rec->progress_ = 0.f; rec->width_ = 0; rec->height_ = 0; + rec->cancel_ = false; // input files if ( rec->files_.size() < 1 ) { @@ -372,6 +373,14 @@ std::string MultiFileRecorder::assemble (MultiFileRecorder *rec) if ( rec->start_record( filename ) ) { + // specify caps for images (same as video, without framerate) + GstCaps *tmp_caps = gst_caps_copy( gst_app_src_get_caps(rec->src_) ); + GValue v = G_VALUE_INIT; + g_value_init (&v, GST_TYPE_FRACTION); + gst_value_set_fraction (&v, 0, 1); + gst_caps_set_value(tmp_caps, "framerate", &v); + g_value_unset (&v); + // progressing rec->progress_ += inc_; @@ -381,7 +390,7 @@ std::string MultiFileRecorder::assemble (MultiFileRecorder *rec) if ( rec->cancel_ ) break; - if ( rec->add_image( *file, gst_app_src_get_caps(rec->src_) ) ) { + if ( rec->add_image( *file, tmp_caps) ) { // validate file rec->frame_count_++; @@ -414,6 +423,8 @@ std::string MultiFileRecorder::assemble (MultiFileRecorder *rec) Log::Info("MultiFileRecorder %d images encoded (%s).", rec->frame_count_, GstToolkit::time_to_string(rec->timestamp_, GstToolkit::TIME_STRING_READABLE).c_str()); else filename = std::string(); + + gst_caps_unref(tmp_caps); } else filename = std::string(); diff --git a/src/Recorder.cpp b/src/Recorder.cpp index a7969dd..c968cc8 100644 --- a/src/Recorder.cpp +++ b/src/Recorder.cpp @@ -36,6 +36,7 @@ #include "Settings.h" #include "GstToolkit.h" #include "SystemToolkit.h" +#include "MediaPlayer.h" #include "Log.h" #include "Audio.h" @@ -333,7 +334,7 @@ std::string VideoRecorder::init(GstCaps *caps) // apply settings buffering_size_ = MAX( MIN_BUFFER_SIZE, buffering_preset_value[Settings::application.record.buffering_mode]); - frame_duration_ = gst_util_uint64_scale_int (1, GST_SECOND, framerate_preset_value[Settings::application.record.framerate_mode]); + frame_duration_ = gst_util_uint64_scale_int (1, GST_SECOND, MAXI(framerate_preset_value[Settings::application.record.framerate_mode], 15)); timestamp_on_clock_ = Settings::application.record.priority_mode < 1; keyframe_count_ = framerate_preset_value[Settings::application.record.framerate_mode]; @@ -366,7 +367,8 @@ std::string VideoRecorder::init(GstCaps *caps) else { // Add Audio to pipeline - if (!Settings::application.record.audio_device.empty()) { + if ( Settings::application.accept_audio && + !Settings::application.record.audio_device.empty()) { // ensure the Audio manager has the device specified in settings int current_audio = Audio::manager().index(Settings::application.record.audio_device); if (current_audio > -1) { @@ -498,9 +500,15 @@ void VideoRecorder::terminate() Log::Info("Video Recording : try a lower resolution / a lower framerate / a larger buffer size / a faster codec."); } - // remember and inform - Settings::application.recentRecordings.push(filename_); - Log::Notify("Video Recording %s is ready.", filename_.c_str()); + // remember and inform if valid + std::string uri = GstToolkit::filename_to_uri(filename_); + MediaInfo media = MediaPlayer::UriDiscoverer(uri); + if (media.valid && !media.isimage) { + Settings::application.recentRecordings.push(filename_); + Log::Notify("Video Recording %s is ready.", filename_.c_str()); + } + else + Settings::application.recentRecordings.remove(filename_); } std::string VideoRecorder::info() const diff --git a/src/Settings.cpp b/src/Settings.cpp index d4661b7..a6874ab 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -186,7 +186,6 @@ void Settings::Save(uint64_t runtime, const std::string &filename) RecordNode->SetAttribute("profile", application.record.profile); RecordNode->SetAttribute("timeout", application.record.timeout); RecordNode->SetAttribute("delay", application.record.delay); - RecordNode->SetAttribute("resolution_mode", application.record.resolution_mode); RecordNode->SetAttribute("framerate_mode", application.record.framerate_mode); RecordNode->SetAttribute("buffering_mode", application.record.buffering_mode); RecordNode->SetAttribute("priority_mode", application.record.priority_mode); @@ -194,6 +193,12 @@ void Settings::Save(uint64_t runtime, const std::string &filename) RecordNode->SetAttribute("audio_device", application.record.audio_device.c_str()); pRoot->InsertEndChild(RecordNode); + // Image sequence + XMLElement *SequenceNode = xmlDoc.NewElement( "Sequence" ); + SequenceNode->SetAttribute("profile", application.image_sequence.profile); + SequenceNode->SetAttribute("framerate", application.image_sequence.framerate_mode); + pRoot->InsertEndChild(SequenceNode); + // Transition XMLElement *TransitionNode = xmlDoc.NewElement( "Transition" ); TransitionNode->SetAttribute("cross_fade", application.transition.cross_fade); @@ -507,7 +512,6 @@ void Settings::Load(const string &filename) recordnode->QueryIntAttribute("profile", &application.record.profile); recordnode->QueryUnsignedAttribute("timeout", &application.record.timeout); recordnode->QueryIntAttribute("delay", &application.record.delay); - recordnode->QueryIntAttribute("resolution_mode", &application.record.resolution_mode); recordnode->QueryIntAttribute("framerate_mode", &application.record.framerate_mode); recordnode->QueryIntAttribute("buffering_mode", &application.record.buffering_mode); recordnode->QueryIntAttribute("priority_mode", &application.record.priority_mode); @@ -526,6 +530,13 @@ void Settings::Load(const string &filename) application.record.audio_device = ""; } + // Record + XMLElement * sequencenode = pRoot->FirstChildElement("Sequence"); + if (sequencenode != nullptr) { + sequencenode->QueryIntAttribute("profile", &application.image_sequence.profile); + sequencenode->QueryIntAttribute("framerate", &application.image_sequence.framerate_mode); + } + // Source XMLElement * sourceconfnode = pRoot->FirstChildElement("Source"); if (sourceconfnode != nullptr) { diff --git a/src/Settings.h b/src/Settings.h index b933871..85531e8 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -104,7 +104,6 @@ struct RecordConfig int profile; uint timeout; int delay; - int resolution_mode; int framerate_mode; int buffering_mode; int priority_mode; @@ -115,7 +114,6 @@ struct RecordConfig profile = 0; timeout = RECORD_MAX_TIMEOUT; delay = 0; - resolution_mode = 1; framerate_mode = 1; buffering_mode = 2; priority_mode = 1; @@ -318,6 +316,7 @@ struct Application // settings exporters RecordConfig record; + RecordConfig image_sequence; // settings new source SourceConfig source; @@ -380,6 +379,7 @@ struct Application windows[0].h = 930; accept_audio = false; dialogPosition = glm::ivec2(-1, -1); + image_sequence.framerate_mode = 15; } }; diff --git a/src/UserInterfaceManager.cpp b/src/UserInterfaceManager.cpp index 8679f5a..e3bdb9f 100644 --- a/src/UserInterfaceManager.cpp +++ b/src/UserInterfaceManager.cpp @@ -3859,7 +3859,7 @@ void Navigator::RenderNewPannel(const ImVec2 &iconsize) IMAGES_FILES_PATTERN); static MultiFileSequence _numbered_sequence; static MultiFileRecorder _video_recorder; - static int _fps = 25; + static int codec_id = -1; ImGui::Text("Image sequence"); @@ -3874,8 +3874,7 @@ void Navigator::RenderNewPannel(const ImVec2 &iconsize) ImGui::SameLine(); ImGuiToolkit::HelpToolTip("Create a source displaying a sequence of images;\n" ICON_FA_CARET_RIGHT " files numbered consecutively\n" - ICON_FA_CARET_RIGHT " create a video from many images\n" - "Supports PNG, JPG or TIF."); + ICON_FA_CARET_RIGHT " create a video from many images"); // return from thread for folder openning if (_selectImagesDialog.closed()) { @@ -3891,9 +3890,16 @@ void Navigator::RenderNewPannel(const ImVec2 &iconsize) // automatically create a MultiFile Source if possible if (_numbered_sequence.valid()) { + // always come back to propose image sequence when possible + codec_id = -1; + // show source preview available if possible std::string label = BaseToolkit::transliterate( BaseToolkit::common_pattern(sourceSequenceFiles) ); - new_source_preview_.setSource( Mixer::manager().createSourceMultifile(sourceSequenceFiles, _fps), label); - } + new_source_preview_ + .setSource(Mixer::manager().createSourceMultifile(sourceSequenceFiles, + Settings::application.image_sequence.framerate_mode), + label); + } else + codec_id = Settings::application.image_sequence.profile; } // multiple files selected @@ -3907,44 +3913,84 @@ void Navigator::RenderNewPannel(const ImVec2 &iconsize) info.appendf("%d %s", (int) sourceSequenceFiles.size(), _numbered_sequence.codec.c_str()); ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); 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"); + ImGui::SliderInt("Framerate", &Settings::application.image_sequence.framerate_mode, 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); + new_source_preview_ + .setSource(Mixer::manager().createSourceMultifile( + sourceSequenceFiles, + Settings::application.image_sequence.framerate_mode), + label); } } - ImGui::Spacing(); + // select CODEC: decide for gst sequence (codec_id = -1) or encoding a video + ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); + std::string codec_current = codec_id < 0 ? ICON_FA_SORT_NUMERIC_DOWN " Numbered images" + : std::string(ICON_FA_FILM " ") + VideoRecorder::profile_name[codec_id]; + if (ImGui::BeginCombo("##CodecSequence", codec_current.c_str())) { + // special case; if possible, offer to create an image sequence gst source + if (ImGui::Selectable( ICON_FA_SORT_NUMERIC_DOWN " Numbered images", + codec_id < 0, + _numbered_sequence.valid() + ? ImGuiSelectableFlags_None + : ImGuiSelectableFlags_Disabled)) { + // select id of image sequence + codec_id = -1; + // Open source preview for image sequence + if (_numbered_sequence.valid()) { + std::string label = BaseToolkit::transliterate( + BaseToolkit::common_pattern(sourceSequenceFiles)); + new_source_preview_ + .setSource(Mixer::manager().createSourceMultifile( + sourceSequenceFiles, + Settings::application.image_sequence.framerate_mode), + label); + } + } + // always offer to encode a video + for (int i = VideoRecorder::H264_STANDARD; i < VideoRecorder::VP8; ++i) { + std::string label = std::string(ICON_FA_FILM " ") + VideoRecorder::profile_name[i]; + if (ImGui::Selectable(label.c_str(), codec_id == i)) { + // select id of video encoding codec + codec_id = i; + Settings::application.image_sequence.profile = i; + // close source preview (no image sequence) + new_source_preview_.setSource(); + } + } + ImGui::EndCombo(); + } + // Indication + ImGui::SameLine(); + if (_numbered_sequence.valid()) + ImGuiToolkit::HelpToolTip(ICON_FA_SORT_NUMERIC_DOWN " Selected images are numbered consecutively; " + "an image sequence source can be created.\n\n" + ICON_FA_FILM " Alternatively, choose a codec to encode a video with the selected images and create a video source."); + else + ImGuiToolkit::HelpToolTip(ICON_FA_SORT_NUMERIC_DOWN " Selected images are NOT numbered consecutively; " + "it is not possible to create a sequence source.\n\n" + ICON_FA_FILM " Instead, choose a codec to encode a video with the selected images and create a video source."); - // 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); + // if video encoding codec selected + if ( codec_id >= 0 ) + { + // Offer to create video from sequence + ImGui::NewLine(); + if ( ImGui::Button( ICON_FA_FILM " Encode video", ImVec2(ImGui::GetContentRegionAvail().x, 0)) ) { + // start video recorder + _video_recorder.setFiles( sourceSequenceFiles ); + _video_recorder.setFramerate( Settings::application.image_sequence.framerate_mode ); + _video_recorder.setProfile( (VideoRecorder::Profile) Settings::application.image_sequence.profile ); + _video_recorder.start(); + // open dialog + ImGui::OpenPopup(LABEL_VIDEO_SEQUENCE); + } } // video recorder finished: inform and open pannel to import video source from recent recordings @@ -3955,7 +4001,7 @@ void Navigator::RenderNewPannel(const ImVec2 &iconsize) else { Log::Notify("Image sequence saved to %s.", _video_recorder.filename().c_str()); // open the file as new recording -// if (Settings::application.recentRecordings.load_at_start) + // if (Settings::application.recentRecordings.load_at_start) UserInterface::manager().navigator.setNewMedia(Navigator::MEDIA_RECORDING, _video_recorder.filename()); } } @@ -3978,7 +4024,8 @@ void Navigator::RenderNewPannel(const ImVec2 &iconsize) ImGui::ProgressBar(_video_recorder.progress()); ImGui::Spacing(); - if (ImGui::Button(ICON_FA_TIMES " Cancel")) + ImGui::Spacing(); + if (ImGui::Button(ICON_FA_TIMES " Cancel",ImVec2(ImGui::GetContentRegionAvail().x, 0))) _video_recorder.cancel(); ImGui::EndPopup();