mirror of
https://github.com/brunoherbelin/vimix.git
synced 2025-12-05 23:40:02 +01:00
455 lines
16 KiB
C++
455 lines
16 KiB
C++
/*
|
||
* This file is part of vimix - video live mixer
|
||
*
|
||
* **Copyright** (C) 2020-2021 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||
*
|
||
* This program is free software: you can redistribute it and/or modify
|
||
* it under the terms of the GNU General Public License as published by
|
||
* the Free Software Foundation, either version 3 of the License, or
|
||
* (at your option) any later version.
|
||
*
|
||
* This program is distributed in the hope that it will be useful,
|
||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
* GNU General Public License for more details.
|
||
*
|
||
* You should have received a copy of the GNU General Public License
|
||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
**/
|
||
|
||
#include <thread>
|
||
|
||
// Desktop OpenGL function loader
|
||
#include <glad/glad.h>
|
||
|
||
// standalone image loader
|
||
#include <stb_image.h>
|
||
#include <stb_image_write.h>
|
||
|
||
// gstreamer
|
||
#include <gst/gstformat.h>
|
||
#include <gst/video/video.h>
|
||
|
||
#include "Settings.h"
|
||
#include "GstToolkit.h"
|
||
#include "defines.h"
|
||
#include "SystemToolkit.h"
|
||
#include "FrameBuffer.h"
|
||
#include "Log.h"
|
||
|
||
#include "Recorder.h"
|
||
|
||
PNGRecorder::PNGRecorder() : FrameGrabber()
|
||
{
|
||
}
|
||
|
||
void PNGRecorder::init(GstCaps *caps)
|
||
{
|
||
// ignore
|
||
if (caps == nullptr)
|
||
return;
|
||
|
||
// create a gstreamer pipeline
|
||
std::string description = "appsrc name=src ! videoconvert ! pngenc ! filesink name=sink";
|
||
|
||
// parse pipeline descriptor
|
||
GError *error = NULL;
|
||
pipeline_ = gst_parse_launch (description.c_str(), &error);
|
||
if (error != NULL) {
|
||
Log::Warning("PNG Capture Could not construct pipeline %s:\n%s", description.c_str(), error->message);
|
||
g_clear_error (&error);
|
||
finished_ = true;
|
||
return;
|
||
}
|
||
|
||
// verify location path (path is always terminated by the OS dependent separator)
|
||
std::string path = SystemToolkit::path_directory(Settings::application.record.path);
|
||
if (path.empty())
|
||
path = SystemToolkit::home_path();
|
||
filename_ = path + "vimix_" + SystemToolkit::date_time_string() + ".png";
|
||
|
||
// setup file sink
|
||
g_object_set (G_OBJECT (gst_bin_get_by_name (GST_BIN (pipeline_), "sink")),
|
||
"location", filename_.c_str(),
|
||
"sync", FALSE,
|
||
NULL);
|
||
|
||
// setup custom app source
|
||
src_ = GST_APP_SRC( gst_bin_get_by_name (GST_BIN (pipeline_), "src") );
|
||
if (src_) {
|
||
|
||
g_object_set (G_OBJECT (src_),
|
||
"is-live", TRUE,
|
||
NULL);
|
||
|
||
// configure stream
|
||
gst_app_src_set_stream_type( src_, GST_APP_STREAM_TYPE_STREAM);
|
||
gst_app_src_set_latency( src_, -1, 0);
|
||
|
||
// Direct encoding (no buffering)
|
||
gst_app_src_set_max_bytes( src_, 0 );
|
||
|
||
// instruct src to use the required caps
|
||
caps_ = gst_caps_copy( caps );
|
||
gst_app_src_set_caps (src_, caps_);
|
||
|
||
// setup callbacks
|
||
GstAppSrcCallbacks callbacks;
|
||
callbacks.need_data = FrameGrabber::callback_need_data;
|
||
callbacks.enough_data = FrameGrabber::callback_enough_data;
|
||
callbacks.seek_data = NULL; // stream type is not seekable
|
||
gst_app_src_set_callbacks (src_, &callbacks, this, NULL);
|
||
|
||
}
|
||
else {
|
||
Log::Warning("PNG Capture Could not configure source");
|
||
finished_ = true;
|
||
return;
|
||
}
|
||
|
||
// start pipeline
|
||
GstStateChangeReturn ret = gst_element_set_state (pipeline_, GST_STATE_PLAYING);
|
||
if (ret == GST_STATE_CHANGE_FAILURE) {
|
||
Log::Warning("PNG Capture Could not record %s", filename_.c_str());
|
||
finished_ = true;
|
||
return;
|
||
}
|
||
|
||
// all good
|
||
Log::Info("PNG Capture started.");
|
||
|
||
// start recording !!
|
||
active_ = true;
|
||
}
|
||
|
||
void PNGRecorder::terminate()
|
||
{
|
||
Log::Notify("PNG Capture %s is ready.", filename_.c_str());
|
||
}
|
||
|
||
void PNGRecorder::addFrame(GstBuffer *buffer, GstCaps *caps)
|
||
{
|
||
FrameGrabber::addFrame(buffer, caps);
|
||
|
||
// PNG Recorder specific :
|
||
// stop after one frame
|
||
if (frame_count_ > 0) {
|
||
stop();
|
||
}
|
||
}
|
||
|
||
|
||
const char* VideoRecorder::profile_name[VideoRecorder::DEFAULT] = {
|
||
"H264 (Realtime)",
|
||
"H264 (High 4:4:4)",
|
||
"H265 (Realtime)",
|
||
"H265 (HQ)",
|
||
"ProRes (Standard)",
|
||
"ProRes (HQ 4444)",
|
||
"WebM VP8 (Realtime)",
|
||
"Multiple JPEG"
|
||
};
|
||
|
||
const std::vector<std::string> VideoRecorder::profile_description {
|
||
// Control x264 encoder quality :
|
||
// pass
|
||
// quant (4) – Constant Quantizer
|
||
// qual (5) – Constant Quality
|
||
// quantizer
|
||
// The total range is from 0 to 51, where 0 is lossless, 18 can be considered ‘visually lossless’,
|
||
// and 51 is terrible quality. A sane range is 18-26, and the default is 23.
|
||
// speed-preset
|
||
// ultrafast (1)
|
||
// superfast (2)
|
||
// veryfast (3)
|
||
// faster (4)
|
||
// fast (5)
|
||
"video/x-raw, format=I420 ! x264enc tune=\"zerolatency\" pass=4 quantizer=22 speed-preset=2 ! video/x-h264, profile=baseline ! h264parse ! ",
|
||
"video/x-raw, format=Y444_10LE ! x264enc pass=4 quantizer=18 speed-preset=3 ! video/x-h264, profile=(string)high-4:4:4 ! h264parse ! ",
|
||
// Control x265 encoder quality :
|
||
// NB: apparently x265 only accepts I420 format :(
|
||
// speed-preset
|
||
// superfast (2)
|
||
// veryfast (3)
|
||
// faster (4)
|
||
// fast (5)
|
||
// Tune
|
||
// psnr (1)
|
||
// ssim (2) DEFAULT
|
||
// grain (3)
|
||
// zerolatency (4) Encoder latency is removed
|
||
// fastdecode (5)
|
||
// animation (6) optimize the encode quality for animation content without impacting the encode speed
|
||
// crf Quality-controlled variable bitrate [0 51]
|
||
// default 28
|
||
// 24 for x265 should be visually transparent; anything lower will probably just waste file size
|
||
"video/x-raw, format=I420 ! x265enc tune=2 speed-preset=2 option-string=\"crf=24\" ! video/x-h265, profile=(string)main ! h265parse ! ",
|
||
"video/x-raw, format=I420 ! x265enc tune=6 speed-preset=2 option-string=\"crf=12\" ! video/x-h265, profile=(string)main ! h265parse ! ",
|
||
// Apple ProRes encoding parameters
|
||
// pass
|
||
// cbr (0) – Constant Bitrate Encoding
|
||
// quant (2) – Constant Quantizer
|
||
// pass1 (512) – VBR Encoding - Pass 1
|
||
// profile
|
||
// 0 ‘proxy’ 45Mbps YUV 4:2:2
|
||
// 1 ‘lt’ 102Mbps YUV 4:2:2
|
||
// 2 ‘standard’ 147Mbps YUV 4:2:2
|
||
// 3 ‘hq’ 220Mbps YUV 4:2:2
|
||
// 4 ‘4444’ 330Mbps YUVA 4:4:4:4
|
||
// quant-mat
|
||
// -1 auto
|
||
// 0 proxy
|
||
// 2 lt
|
||
// 3 standard
|
||
// 4 hq
|
||
// 6 default
|
||
"video/x-raw, format=I422_10LE ! avenc_prores_ks pass=2 bits_per_mb=8000 profile=2 quant-mat=6 quantizer=8 ! ",
|
||
"video/x-raw, format=Y444_10LE ! avenc_prores_ks pass=2 bits_per_mb=8000 profile=4 quant-mat=6 quantizer=4 ! ",
|
||
// VP8 WebM encoding
|
||
// deadline per frame (usec)
|
||
// 0=best,
|
||
// 1=realtime
|
||
// see https://www.webmproject.org/docs/encoder-parameters/
|
||
// "vp8enc end-usage=cbr deadline=1 cpu-used=8 threads=4 target-bitrate=400000 undershoot=95 "
|
||
// "buffer-size=6000 buffer-initial-size=4000 buffer-optimal-size=5000 "
|
||
// "keyframe-max-dist=999999 min-quantizer=4 max-quantizer=50 ! ",
|
||
"vp8enc end-usage=vbr deadline=1 cpu-used=8 threads=4 target-bitrate=400000 keyframe-max-dist=360 "
|
||
"token-partitions=2 static-threshold=1000 min-quantizer=4 max-quantizer=20 ! ",
|
||
// JPEG encoding
|
||
"jpegenc idct-method=float ! "
|
||
};
|
||
|
||
|
||
#if GST_GL_HAVE_PLATFORM_GLX
|
||
// under GLX (Linux), gstreamer might have nvidia encoders
|
||
const char* VideoRecorder::hardware_encoder[VideoRecorder::DEFAULT] = {
|
||
"nvh264enc",
|
||
"nvh264enc",
|
||
"nvh265enc",
|
||
"nvh265enc",
|
||
"", "", "", ""
|
||
};
|
||
|
||
const std::vector<std::string> VideoRecorder::hardware_profile_description {
|
||
// qp-const Constant quantizer (-1 = from NVENC preset)
|
||
// Range: -1 - 51 Default: -1
|
||
// rc-mode Rate Control Mode
|
||
// (0): default - Default
|
||
// (1): constqp - Constant Quantization
|
||
// (2): cbr - Constant Bit Rate
|
||
// (3): vbr - Variable Bit Rate
|
||
// (4): vbr-minqp - Variable Bit Rate (with minimum quantization parameter, DEPRECATED)
|
||
// (5): cbr-ld-hq - Low-Delay CBR, High Quality
|
||
// (6): cbr-hq - CBR, High Quality (slower)
|
||
// (7): vbr-hq - VBR, High Quality (slower)
|
||
// Control nvh264enc encoder
|
||
"video/x-raw, format=RGBA ! nvh264enc rc-mode=1 zerolatency=true ! video/x-h264, profile=(string)main ! h264parse ! ",
|
||
"video/x-raw, format=RGBA ! nvh264enc rc-mode=1 qp-const=18 ! video/x-h264, profile=(string)high-4:4:4 ! h264parse ! ",
|
||
// Control nvh265enc encoder
|
||
"video/x-raw, format=RGBA ! nvh265enc rc-mode=1 zerolatency=true ! video/x-h265, profile=(string)main-10 ! h265parse ! ",
|
||
"video/x-raw, format=RGBA ! nvh265enc rc-mode=1 qp-const=18 ! video/x-h265, profile=(string)main-444 ! h265parse ! ",
|
||
"", "", "", ""
|
||
};
|
||
|
||
#elif GST_GL_HAVE_PLATFORM_CGL
|
||
// under CGL (Mac), gstreamer might have the VideoToolbox
|
||
const char* VideoRecorder::hardware_encoder[VideoRecorder::DEFAULT] = {
|
||
"vtenc_h264_hw",
|
||
"vtenc_h264_hw",
|
||
"", "", "", "", "", ""
|
||
};
|
||
|
||
const std::vector<std::string> VideoRecorder::hardware_profile_description {
|
||
// Control vtenc_h264_hw encoder
|
||
"video/x-raw, format=I420 ! vtenc_h264_hw realtime=1 allow-frame-reordering=0 ! h264parse ! ",
|
||
"video/x-raw, format=UYVY ! vtenc_h264_hw realtime=1 allow-frame-reordering=0 quality=0.9 ! h264parse ! ",
|
||
"", "", "", "", "", ""
|
||
};
|
||
|
||
#else
|
||
const char* VideoRecorder::hardware_encoder[VideoRecorder::DEFAULT] = {
|
||
"", "", "", "", "", "", "", ""
|
||
};
|
||
const std::vector<std::string> VideoRecorder::hardware_profile_description {
|
||
"", "", "", "", "", "", "", ""
|
||
};
|
||
#endif
|
||
|
||
|
||
|
||
const char* VideoRecorder::buffering_preset_name[6] = { "Minimum", "100 MB", "200 MB", "500 MB", "1 GB", "2 GB" };
|
||
const guint64 VideoRecorder::buffering_preset_value[6] = { MIN_BUFFER_SIZE, 104857600, 209715200, 524288000, 1073741824, 2147483648 };
|
||
|
||
const char* VideoRecorder::framerate_preset_name[3] = { "15 FPS", "25 FPS", "30 FPS" };
|
||
const gint VideoRecorder::framerate_preset_value[3] = { 15, 25, 30 };
|
||
|
||
|
||
VideoRecorder::VideoRecorder() : FrameGrabber()
|
||
{
|
||
|
||
}
|
||
|
||
void VideoRecorder::init(GstCaps *caps)
|
||
{
|
||
// ignore
|
||
if (caps == nullptr)
|
||
return;
|
||
|
||
// 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]);
|
||
timestamp_on_clock_ = Settings::application.record.priority_mode < 1;
|
||
|
||
// create a gstreamer pipeline
|
||
std::string description = "appsrc name=src ! videoconvert ! ";
|
||
if (Settings::application.record.profile < 0 || Settings::application.record.profile >= DEFAULT)
|
||
Settings::application.record.profile = H264_STANDARD;
|
||
|
||
// test for a hardware accelerated encoder
|
||
if (Settings::application.render.gpu_decoding &&
|
||
#if GST_GL_HAVE_PLATFORM_GLX
|
||
|
||
glGetString(GL_VENDOR)[0] == 'N' && glGetString(GL_VENDOR)[1] == 'V' && // TODO; hack to test for NVIDIA GPU support
|
||
#endif
|
||
GstToolkit::has_feature(hardware_encoder[Settings::application.record.profile]) ) {
|
||
|
||
description += hardware_profile_description[Settings::application.record.profile];
|
||
Log::Info("Video Recording using hardware accelerated encoder (%s)", hardware_encoder[Settings::application.record.profile]);
|
||
}
|
||
// revert to software encoder
|
||
else
|
||
description += profile_description[Settings::application.record.profile];
|
||
|
||
// verify location path (path is always terminated by the OS dependent separator)
|
||
std::string path = SystemToolkit::path_directory(Settings::application.record.path);
|
||
if (path.empty())
|
||
path = SystemToolkit::home_path();
|
||
|
||
// setup filename & muxer
|
||
if( Settings::application.record.profile == JPEG_MULTI) {
|
||
std::string folder = path + "vimix_" + SystemToolkit::date_time_string();
|
||
filename_ = SystemToolkit::full_filename(folder, "%05d.jpg");
|
||
if (SystemToolkit::create_directory(folder))
|
||
description += "multifilesink name=sink";
|
||
}
|
||
else if( Settings::application.record.profile == VP8) {
|
||
filename_ = path + "vimix_" + SystemToolkit::date_time_string() + ".webm";
|
||
description += "webmmux ! filesink name=sink";
|
||
}
|
||
else {
|
||
filename_ = path + "vimix_" + SystemToolkit::date_time_string() + ".mov";
|
||
description += "qtmux ! filesink name=sink";
|
||
}
|
||
|
||
// parse pipeline descriptor
|
||
GError *error = NULL;
|
||
pipeline_ = gst_parse_launch (description.c_str(), &error);
|
||
if (error != NULL) {
|
||
Log::Info("Video Recording : Could not construct pipeline %s\n%s", description.c_str(), error->message);
|
||
Log::Warning("Video Recording : Failed to initiate GStreamer.");
|
||
g_clear_error (&error);
|
||
finished_ = true;
|
||
return;
|
||
}
|
||
|
||
// setup file sink
|
||
g_object_set (G_OBJECT (gst_bin_get_by_name (GST_BIN (pipeline_), "sink")),
|
||
"location", filename_.c_str(),
|
||
"sync", FALSE,
|
||
NULL);
|
||
|
||
// setup custom app source
|
||
src_ = GST_APP_SRC( gst_bin_get_by_name (GST_BIN (pipeline_), "src") );
|
||
if (src_) {
|
||
|
||
g_object_set (G_OBJECT (src_),
|
||
"is-live", TRUE,
|
||
"format", GST_FORMAT_TIME,
|
||
NULL);
|
||
|
||
if (timestamp_on_clock_)
|
||
g_object_set (G_OBJECT (src_),"do-timestamp", TRUE,NULL);
|
||
|
||
// configure stream
|
||
gst_app_src_set_stream_type( src_, GST_APP_STREAM_TYPE_STREAM);
|
||
gst_app_src_set_latency( src_, -1, 0);
|
||
|
||
// Set buffer size
|
||
gst_app_src_set_max_bytes( src_, buffering_size_);
|
||
|
||
// specify recorder framerate in the given caps
|
||
GstCaps *tmp = gst_caps_copy( caps );
|
||
GValue v = { 0, };
|
||
g_value_init (&v, GST_TYPE_FRACTION);
|
||
gst_value_set_fraction (&v, framerate_preset_value[Settings::application.record.framerate_mode], 1);
|
||
gst_caps_set_value(tmp, "framerate", &v);
|
||
g_value_unset (&v);
|
||
|
||
// instruct src to use the caps
|
||
caps_ = gst_caps_copy( tmp );
|
||
gst_app_src_set_caps (src_, caps_);
|
||
gst_caps_unref (tmp);
|
||
|
||
// setup callbacks
|
||
GstAppSrcCallbacks callbacks;
|
||
callbacks.need_data = FrameGrabber::callback_need_data;
|
||
callbacks.enough_data = FrameGrabber::callback_enough_data;
|
||
callbacks.seek_data = NULL; // stream type is not seekable
|
||
gst_app_src_set_callbacks (src_, &callbacks, this, NULL);
|
||
|
||
}
|
||
else {
|
||
Log::Warning("Video Recording : Failed to configure frame grabber.");
|
||
finished_ = true;
|
||
return;
|
||
}
|
||
|
||
// start recording
|
||
GstStateChangeReturn ret = gst_element_set_state (pipeline_, GST_STATE_PLAYING);
|
||
if (ret == GST_STATE_CHANGE_FAILURE) {
|
||
Log::Warning("Video Recording : Failed to start frame grabber.");
|
||
finished_ = true;
|
||
return;
|
||
}
|
||
|
||
// all good
|
||
Log::Info("Video Recording started (%s)", profile_name[Settings::application.record.profile]);
|
||
|
||
// start recording !!
|
||
active_ = true;
|
||
}
|
||
|
||
void VideoRecorder::terminate()
|
||
{
|
||
// stop the pipeline (again)
|
||
gst_element_set_state (pipeline_, GST_STATE_NULL);
|
||
|
||
// statistics on expected number of frames
|
||
guint64 N = MAX( (guint64) duration_ / (guint64) frame_duration_, frame_count_);
|
||
float loss = 100.f * ((float) (N - frame_count_) ) / (float) N;
|
||
Log::Info("Video Recording : %ld frames captured in %s (aming for %ld, %.0f%% lost)",
|
||
frame_count_, GstToolkit::time_to_string(duration_, GstToolkit::TIME_STRING_READABLE).c_str(), N, loss);
|
||
|
||
// warn user if more than 10% lost
|
||
if (loss > 10.f) {
|
||
if (timestamp_on_clock_)
|
||
Log::Warning("Video Recording lost %.0f%% of frames: framerate could not be maintained at %ld FPS.", loss, GST_SECOND / frame_duration_);
|
||
else
|
||
Log::Warning("Video Recording lost %.0f%% of frames: video is only %s long.",
|
||
loss, GstToolkit::time_to_string(timestamp_, GstToolkit::TIME_STRING_READABLE).c_str());
|
||
Log::Info("Video Recording : try a lower resolution / a lower framerate / a larger buffer size / a faster codec.");
|
||
}
|
||
|
||
Log::Notify("Video Recording %s is ready.", filename_.c_str());
|
||
}
|
||
|
||
std::string VideoRecorder::info() const
|
||
{
|
||
if (active_)
|
||
return FrameGrabber::info();
|
||
else if (!endofstream_)
|
||
return "Saving file...";
|
||
else
|
||
return "...";
|
||
}
|