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
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();

View File

@@ -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

View File

@@ -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) {

View File

@@ -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;
}
};

View File

@@ -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();