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.
This commit is contained in:
Bruno Herbelin
2024-05-01 23:29:08 +02:00
parent a4f8d46d69
commit a46e68f145
5 changed files with 131 additions and 54 deletions

View File

@@ -79,13 +79,9 @@ bool MultiFileRecorder::add_image (const std::string &image_filename, GstCaps *c
// set flag to only read VIDEO // set flag to only read VIDEO
g_object_set(G_OBJECT(img_pipeline), "flags", 0x00000001, NULL); g_object_set(G_OBJECT(img_pipeline), "flags", 0x00000001, NULL);
// instruct sink to use the required caps (without framerate) // instruct sink to use the required caps
GstCaps *sinkcaps = gst_caps_copy(caps);
GValue v = {GST_TYPE_FRACTION, {{0}, {1}}};
gst_caps_set_value(sinkcaps, "framerate", &v);
GstElement *sink = gst_element_factory_make("appsink", "imgsink"); 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 // set playbin sink
g_object_set(G_OBJECT(img_pipeline), "video-sink", sink, NULL); 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 */ /* Clean up */
gst_caps_unref(caps);
gst_sample_unref(sample); gst_sample_unref(sample);
gst_element_set_state(img_pipeline, GST_STATE_NULL); gst_element_set_state(img_pipeline, GST_STATE_NULL);
gst_object_unref(GST_OBJECT(img_pipeline)); 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 && if (Settings::application.render.gpu_decoding && (int) VideoRecorder::hardware_encoder.size() > 0 &&
GstToolkit::has_feature(VideoRecorder::hardware_encoder[profile_]) ) { 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()); Log::Info("MultiFileRecorder use hardware accelerated encoder (%s)", VideoRecorder::hardware_encoder[profile_].c_str());
} }
// revert to software encoder // revert to software encoder
@@ -317,8 +312,13 @@ bool MultiFileRecorder::finished ()
// get the filename from encoder // get the filename from encoder
filename_ = promises_.back().get(); filename_ = promises_.back().get();
if (!filename_.empty()) { if (!filename_.empty()) {
// save path location // 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_); Settings::application.recentRecordings.push(filename_);
else
Settings::application.recentRecordings.remove(filename_);
} }
// done with this recoding // done with this recoding
promises_.pop_back(); promises_.pop_back();
@@ -336,6 +336,7 @@ std::string MultiFileRecorder::assemble (MultiFileRecorder *rec)
rec->progress_ = 0.f; rec->progress_ = 0.f;
rec->width_ = 0; rec->width_ = 0;
rec->height_ = 0; rec->height_ = 0;
rec->cancel_ = false;
// input files // input files
if ( rec->files_.size() < 1 ) { if ( rec->files_.size() < 1 ) {
@@ -372,6 +373,14 @@ std::string MultiFileRecorder::assemble (MultiFileRecorder *rec)
if ( rec->start_record( filename ) ) 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 // progressing
rec->progress_ += inc_; rec->progress_ += inc_;
@@ -381,7 +390,7 @@ std::string MultiFileRecorder::assemble (MultiFileRecorder *rec)
if ( rec->cancel_ ) if ( rec->cancel_ )
break; break;
if ( rec->add_image( *file, gst_app_src_get_caps(rec->src_) ) ) { if ( rec->add_image( *file, tmp_caps) ) {
// validate file // validate file
rec->frame_count_++; 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()); Log::Info("MultiFileRecorder %d images encoded (%s).", rec->frame_count_, GstToolkit::time_to_string(rec->timestamp_, GstToolkit::TIME_STRING_READABLE).c_str());
else else
filename = std::string(); filename = std::string();
gst_caps_unref(tmp_caps);
} }
else else
filename = std::string(); filename = std::string();

View File

@@ -36,6 +36,7 @@
#include "Settings.h" #include "Settings.h"
#include "GstToolkit.h" #include "GstToolkit.h"
#include "SystemToolkit.h" #include "SystemToolkit.h"
#include "MediaPlayer.h"
#include "Log.h" #include "Log.h"
#include "Audio.h" #include "Audio.h"
@@ -333,7 +334,7 @@ std::string VideoRecorder::init(GstCaps *caps)
// apply settings // apply settings
buffering_size_ = MAX( MIN_BUFFER_SIZE, buffering_preset_value[Settings::application.record.buffering_mode]); 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; timestamp_on_clock_ = Settings::application.record.priority_mode < 1;
keyframe_count_ = framerate_preset_value[Settings::application.record.framerate_mode]; keyframe_count_ = framerate_preset_value[Settings::application.record.framerate_mode];
@@ -366,7 +367,8 @@ std::string VideoRecorder::init(GstCaps *caps)
else { else {
// Add Audio to pipeline // 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 // ensure the Audio manager has the device specified in settings
int current_audio = Audio::manager().index(Settings::application.record.audio_device); int current_audio = Audio::manager().index(Settings::application.record.audio_device);
if (current_audio > -1) { if (current_audio > -1) {
@@ -498,10 +500,16 @@ void VideoRecorder::terminate()
Log::Info("Video Recording : try a lower resolution / a lower framerate / a larger buffer size / a faster codec."); Log::Info("Video Recording : try a lower resolution / a lower framerate / a larger buffer size / a faster codec.");
} }
// remember and inform // 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_); Settings::application.recentRecordings.push(filename_);
Log::Notify("Video Recording %s is ready.", filename_.c_str()); Log::Notify("Video Recording %s is ready.", filename_.c_str());
} }
else
Settings::application.recentRecordings.remove(filename_);
}
std::string VideoRecorder::info() const std::string VideoRecorder::info() const
{ {

View File

@@ -186,7 +186,6 @@ void Settings::Save(uint64_t runtime, const std::string &filename)
RecordNode->SetAttribute("profile", application.record.profile); RecordNode->SetAttribute("profile", application.record.profile);
RecordNode->SetAttribute("timeout", application.record.timeout); RecordNode->SetAttribute("timeout", application.record.timeout);
RecordNode->SetAttribute("delay", application.record.delay); RecordNode->SetAttribute("delay", application.record.delay);
RecordNode->SetAttribute("resolution_mode", application.record.resolution_mode);
RecordNode->SetAttribute("framerate_mode", application.record.framerate_mode); RecordNode->SetAttribute("framerate_mode", application.record.framerate_mode);
RecordNode->SetAttribute("buffering_mode", application.record.buffering_mode); RecordNode->SetAttribute("buffering_mode", application.record.buffering_mode);
RecordNode->SetAttribute("priority_mode", application.record.priority_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()); RecordNode->SetAttribute("audio_device", application.record.audio_device.c_str());
pRoot->InsertEndChild(RecordNode); 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 // Transition
XMLElement *TransitionNode = xmlDoc.NewElement( "Transition" ); XMLElement *TransitionNode = xmlDoc.NewElement( "Transition" );
TransitionNode->SetAttribute("cross_fade", application.transition.cross_fade); 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->QueryIntAttribute("profile", &application.record.profile);
recordnode->QueryUnsignedAttribute("timeout", &application.record.timeout); recordnode->QueryUnsignedAttribute("timeout", &application.record.timeout);
recordnode->QueryIntAttribute("delay", &application.record.delay); recordnode->QueryIntAttribute("delay", &application.record.delay);
recordnode->QueryIntAttribute("resolution_mode", &application.record.resolution_mode);
recordnode->QueryIntAttribute("framerate_mode", &application.record.framerate_mode); recordnode->QueryIntAttribute("framerate_mode", &application.record.framerate_mode);
recordnode->QueryIntAttribute("buffering_mode", &application.record.buffering_mode); recordnode->QueryIntAttribute("buffering_mode", &application.record.buffering_mode);
recordnode->QueryIntAttribute("priority_mode", &application.record.priority_mode); recordnode->QueryIntAttribute("priority_mode", &application.record.priority_mode);
@@ -526,6 +530,13 @@ void Settings::Load(const string &filename)
application.record.audio_device = ""; 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 // Source
XMLElement * sourceconfnode = pRoot->FirstChildElement("Source"); XMLElement * sourceconfnode = pRoot->FirstChildElement("Source");
if (sourceconfnode != nullptr) { if (sourceconfnode != nullptr) {

View File

@@ -104,7 +104,6 @@ struct RecordConfig
int profile; int profile;
uint timeout; uint timeout;
int delay; int delay;
int resolution_mode;
int framerate_mode; int framerate_mode;
int buffering_mode; int buffering_mode;
int priority_mode; int priority_mode;
@@ -115,7 +114,6 @@ struct RecordConfig
profile = 0; profile = 0;
timeout = RECORD_MAX_TIMEOUT; timeout = RECORD_MAX_TIMEOUT;
delay = 0; delay = 0;
resolution_mode = 1;
framerate_mode = 1; framerate_mode = 1;
buffering_mode = 2; buffering_mode = 2;
priority_mode = 1; priority_mode = 1;
@@ -318,6 +316,7 @@ struct Application
// settings exporters // settings exporters
RecordConfig record; RecordConfig record;
RecordConfig image_sequence;
// settings new source // settings new source
SourceConfig source; SourceConfig source;
@@ -380,6 +379,7 @@ struct Application
windows[0].h = 930; windows[0].h = 930;
accept_audio = false; accept_audio = false;
dialogPosition = glm::ivec2(-1, -1); dialogPosition = glm::ivec2(-1, -1);
image_sequence.framerate_mode = 15;
} }
}; };

View File

@@ -3859,7 +3859,7 @@ void Navigator::RenderNewPannel(const ImVec2 &iconsize)
IMAGES_FILES_PATTERN); IMAGES_FILES_PATTERN);
static MultiFileSequence _numbered_sequence; static MultiFileSequence _numbered_sequence;
static MultiFileRecorder _video_recorder; static MultiFileRecorder _video_recorder;
static int _fps = 25; static int codec_id = -1;
ImGui::Text("Image sequence"); ImGui::Text("Image sequence");
@@ -3874,8 +3874,7 @@ void Navigator::RenderNewPannel(const ImVec2 &iconsize)
ImGui::SameLine(); ImGui::SameLine();
ImGuiToolkit::HelpToolTip("Create a source displaying a sequence of images;\n" ImGuiToolkit::HelpToolTip("Create a source displaying a sequence of images;\n"
ICON_FA_CARET_RIGHT " files numbered consecutively\n" ICON_FA_CARET_RIGHT " files numbered consecutively\n"
ICON_FA_CARET_RIGHT " create a video from many images\n" ICON_FA_CARET_RIGHT " create a video from many images");
"Supports PNG, JPG or TIF.");
// return from thread for folder openning // return from thread for folder openning
if (_selectImagesDialog.closed()) { if (_selectImagesDialog.closed()) {
@@ -3891,9 +3890,16 @@ void Navigator::RenderNewPannel(const ImVec2 &iconsize)
// automatically create a MultiFile Source if possible // automatically create a MultiFile Source if possible
if (_numbered_sequence.valid()) { 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) ); 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 // multiple files selected
@@ -3907,45 +3913,85 @@ void Navigator::RenderNewPannel(const ImVec2 &iconsize)
info.appendf("%d %s", (int) sourceSequenceFiles.size(), _numbered_sequence.codec.c_str()); info.appendf("%d %s", (int) sourceSequenceFiles.size(), _numbered_sequence.codec.c_str());
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
ImGui::InputText("Images", (char *)info.c_str(), info.size(), ImGuiInputTextFlags_ReadOnly); 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); 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 // set framerate
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); 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 (ImGui::IsItemDeactivatedAfterEdit()){
if (new_source_preview_.filled()) { if (new_source_preview_.filled()) {
std::string label = BaseToolkit::transliterate( BaseToolkit::common_pattern(sourceSequenceFiles) ); 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.");
// if video encoding codec selected
if ( codec_id >= 0 )
{
// Offer to create video from sequence // Offer to create video from sequence
if ( ImGui::Button( ICON_FA_FILM " Make a video", ImVec2(ImGui::GetContentRegionAvail().x, 0)) ) { ImGui::NewLine();
if ( ImGui::Button( ICON_FA_FILM " Encode video", ImVec2(ImGui::GetContentRegionAvail().x, 0)) ) {
// start video recorder // start video recorder
_video_recorder.setFiles( sourceSequenceFiles ); _video_recorder.setFiles( sourceSequenceFiles );
_video_recorder.setFramerate( _fps ); _video_recorder.setFramerate( Settings::application.image_sequence.framerate_mode );
_video_recorder.setProfile( (VideoRecorder::Profile) Settings::application.record.profile ); _video_recorder.setProfile( (VideoRecorder::Profile) Settings::application.image_sequence.profile );
_video_recorder.start(); _video_recorder.start();
// dialog // open dialog
ImGui::OpenPopup(LABEL_VIDEO_SEQUENCE); ImGui::OpenPopup(LABEL_VIDEO_SEQUENCE);
} }
}
// video recorder finished: inform and open pannel to import video source from recent recordings // video recorder finished: inform and open pannel to import video source from recent recordings
if ( _video_recorder.finished() ) { if ( _video_recorder.finished() ) {
@@ -3978,7 +4024,8 @@ void Navigator::RenderNewPannel(const ImVec2 &iconsize)
ImGui::ProgressBar(_video_recorder.progress()); ImGui::ProgressBar(_video_recorder.progress());
ImGui::Spacing(); 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(); _video_recorder.cancel();
ImGui::EndPopup(); ImGui::EndPopup();