mirror of
https://github.com/brunoherbelin/vimix.git
synced 2025-12-05 15:30:00 +01:00
Add Transcoder class for video transcoding functionality and integrate into GUI
This commit is contained in:
@@ -163,6 +163,7 @@ if (PKG_CONFIG_FOUND)
|
||||
gstreamer-audio-1.0
|
||||
gstreamer-video-1.0
|
||||
gstreamer-gl-1.0
|
||||
gstreamer-transcoder-1.0
|
||||
)
|
||||
|
||||
include_directories(
|
||||
|
||||
192
docs/TRANSCODER_USAGE.md
Normal file
192
docs/TRANSCODER_USAGE.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# Transcoder Usage
|
||||
|
||||
The `Transcoder` class provides simple video transcoding functionality using GStreamer.
|
||||
|
||||
## Features
|
||||
|
||||
- Transcodes any video format to H.264/MP4
|
||||
- Automatically generates output filename (non-destructive)
|
||||
- Provides progress monitoring
|
||||
- Asynchronous operation
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```cpp
|
||||
#include "Transcoder.h"
|
||||
|
||||
// Create transcoder with input file
|
||||
Transcoder transcoder("/path/to/input/video.mov");
|
||||
|
||||
// Output filename is automatically generated
|
||||
std::cout << "Output will be: " << transcoder.outputFilename() << std::endl;
|
||||
// Output: /path/to/input/video_transcoded.mp4
|
||||
|
||||
// Start transcoding with default options
|
||||
if (!transcoder.start()) {
|
||||
std::cerr << "Failed to start: " << transcoder.error() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Monitor progress
|
||||
while (!transcoder.finished()) {
|
||||
double progress = transcoder.progress(); // 0.0 to 1.0
|
||||
std::cout << "Progress: " << (progress * 100) << "%" << std::endl;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
}
|
||||
|
||||
// Check result
|
||||
if (transcoder.success()) {
|
||||
std::cout << "Success! Output: " << transcoder.outputFilename() << std::endl;
|
||||
} else {
|
||||
std::cerr << "Failed: " << transcoder.error() << std::endl;
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Usage with Options
|
||||
|
||||
```cpp
|
||||
#include "Transcoder.h"
|
||||
|
||||
// Create transcoder
|
||||
Transcoder transcoder("/path/to/animation.mov");
|
||||
|
||||
// Configure transcoding options
|
||||
TranscoderOptions options;
|
||||
options.force_keyframes = true; // Keyframe every second for easier editing
|
||||
options.psy_tune = PsyTuning::ANIMATION; // Optimize for animation content
|
||||
|
||||
// Start with options
|
||||
if (!transcoder.start(options)) {
|
||||
std::cerr << "Failed: " << transcoder.error() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Wait for completion...
|
||||
while (!transcoder.finished()) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
}
|
||||
```
|
||||
|
||||
## Common Configuration Examples
|
||||
|
||||
### For Video Editing (Maximum Keyframes)
|
||||
```cpp
|
||||
TranscoderOptions options;
|
||||
options.force_keyframes = true; // Keyframe every ~1 second
|
||||
// Makes timeline scrubbing and frame-accurate editing much easier
|
||||
transcoder.start(options);
|
||||
```
|
||||
|
||||
### For Film Content
|
||||
```cpp
|
||||
TranscoderOptions options;
|
||||
options.psy_tune = PsyTuning::FILM; // Optimized for film/camera footage
|
||||
transcoder.start(options);
|
||||
```
|
||||
|
||||
### For Animation
|
||||
```cpp
|
||||
TranscoderOptions options;
|
||||
options.psy_tune = PsyTuning::ANIMATION; // Optimized for flat colors and sharp edges
|
||||
transcoder.start(options);
|
||||
```
|
||||
|
||||
### For Grain Preservation
|
||||
```cpp
|
||||
TranscoderOptions options;
|
||||
options.psy_tune = PsyTuning::GRAIN; // Preserve film grain detail
|
||||
transcoder.start(options);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Constructor
|
||||
```cpp
|
||||
Transcoder(const std::string& input_filename)
|
||||
```
|
||||
Creates a transcoder for the specified input file. Output filename is automatically generated.
|
||||
|
||||
### Enums and Structs
|
||||
|
||||
#### `PsyTuning` enum
|
||||
Psycho-visual tuning options for x264 encoder:
|
||||
- `NONE` - No specific tuning (default)
|
||||
- `FILM` - Optimize for film/live action content
|
||||
- `ANIMATION` - Optimize for animation with flat colors and sharp edges
|
||||
- `GRAIN` - Preserve film grain detail
|
||||
- `STILL_IMAGE` - Optimize for slideshow-style content
|
||||
|
||||
#### `TranscoderOptions` struct
|
||||
Configuration options for transcoding:
|
||||
- `bool force_keyframes` - Force keyframe every ~1 second (default: false)
|
||||
- When enabled: sets `key-int-max=30` (keyframe every second at 30fps)
|
||||
- When disabled: sets `key-int-max=250` (default, less frequent)
|
||||
- Useful for video editing to enable frame-accurate seeking
|
||||
- `PsyTuning psy_tune` - Psycho-visual tuning preset (default: NONE)
|
||||
|
||||
### Methods
|
||||
|
||||
#### `bool start(const TranscoderOptions& options = TranscoderOptions())`
|
||||
Start the transcoding process with optional configuration.
|
||||
- Parameters:
|
||||
- `options` - Transcoding configuration (keyframes, tuning, etc.)
|
||||
- Returns: `true` if started successfully, `false` on error
|
||||
|
||||
#### `bool finished() const`
|
||||
Check if transcoding has completed.
|
||||
- Returns: `true` when transcoding is done (success or error)
|
||||
|
||||
#### `bool success() const`
|
||||
Check if transcoding completed successfully.
|
||||
- Returns: `true` only if finished AND successful
|
||||
|
||||
#### `double progress() const`
|
||||
Get current transcoding progress.
|
||||
- Returns: Value between 0.0 (starting) and 1.0 (complete)
|
||||
|
||||
#### `const std::string& inputFilename() const`
|
||||
Get the input filename.
|
||||
|
||||
#### `const std::string& outputFilename() const`
|
||||
Get the generated output filename.
|
||||
|
||||
#### `const std::string& error() const`
|
||||
Get error message if transcoding failed.
|
||||
- Returns: Error message string, empty if no error
|
||||
|
||||
## Output Filename Generation
|
||||
|
||||
The transcoder automatically generates output filenames:
|
||||
- Input: `/path/to/video.mov`
|
||||
- Output: `/path/to/video_transcoded.mp4`
|
||||
|
||||
If the file exists, it adds a number:
|
||||
- `/path/to/video_transcoded_1.mp4`
|
||||
- `/path/to/video_transcoded_2.mp4`
|
||||
- etc.
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Pipeline
|
||||
The transcoder uses the following GStreamer pipeline:
|
||||
```
|
||||
filesrc → decodebin → videoconvert → x264enc → mp4mux → filesink
|
||||
├→ audioconvert → avenc_aac ┘
|
||||
```
|
||||
|
||||
### Encoding Settings
|
||||
- Video codec: H.264 (x264enc)
|
||||
- Audio codec: AAC (avenc_aac)
|
||||
- Container: MP4
|
||||
- Speed preset: medium
|
||||
- Keyframes: Configurable via `force_keyframes` option
|
||||
- Default: `key-int-max=250` (keyframe every 250 frames)
|
||||
- Force mode: `key-int-max=30` (keyframe every 30 frames, ~1 second at 30fps)
|
||||
- Psycho-visual tuning: Configurable via `psy_tune` option
|
||||
|
||||
### Requirements
|
||||
- GStreamer 1.0+
|
||||
- gstreamer-base
|
||||
- gstreamer-pbutils
|
||||
- x264 encoder plugin
|
||||
- AAC encoder plugin
|
||||
@@ -48,6 +48,7 @@ set(VMIX_SRCS
|
||||
Recorder.cpp
|
||||
Streamer.cpp
|
||||
Loopback.cpp
|
||||
Transcoder.cpp
|
||||
Settings.cpp
|
||||
Screenshot.cpp
|
||||
Resource.cpp
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
#include "ActionManager.h"
|
||||
#include "Mixer.h"
|
||||
#include "ControlManager.h"
|
||||
#include "Transcoder.h"
|
||||
|
||||
#include "imgui.h"
|
||||
#include "ImGuiToolkit.h"
|
||||
@@ -732,6 +733,98 @@ void ImGuiVisitor::visit (MediaSource& s)
|
||||
ImGui::TextDisabled("Hardware decoding disabled");
|
||||
}
|
||||
|
||||
// transcoding panel
|
||||
static Transcoder *transcoder = nullptr;
|
||||
static bool _transcoding = false;
|
||||
|
||||
if (!_transcoding) {
|
||||
if (ImGui::Button(ICON_FA_FILM " Transcoding", ImVec2(IMGUI_RIGHT_ALIGN,0)))
|
||||
_transcoding = true;
|
||||
}
|
||||
|
||||
if (_transcoding || transcoder != nullptr) {
|
||||
|
||||
float w_height = 6.1f * ImGui::GetFrameHeightWithSpacing();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetColorU32(ImGuiCol_PopupBg));
|
||||
ImGui::BeginChild("transcode_child", ImVec2(0, w_height),
|
||||
true, ImGuiWindowFlags_MenuBar);
|
||||
|
||||
if (ImGui::BeginMenuBar())
|
||||
{
|
||||
if (transcoder != nullptr) {
|
||||
ImGuiToolkit::Icon(12,11);
|
||||
} else {
|
||||
if (ImGuiToolkit::IconButton(4,16))
|
||||
_transcoding = false;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::Text(ICON_FA_FILM " Transcoding ");
|
||||
ImGui::EndMenuBar();
|
||||
}
|
||||
|
||||
static bool _optim_kf = true;
|
||||
static bool _optim_still = false;
|
||||
static bool _reduce_size = false;
|
||||
static bool _no_audio = false;
|
||||
|
||||
ImGuiToolkit::ButtonSwitch( "Backward playback", &_optim_kf);
|
||||
ImGuiToolkit::ButtonSwitch( "Still images content", &_optim_still);
|
||||
ImGuiToolkit::ButtonSwitch( "Remove audio", &_no_audio);
|
||||
ImGuiToolkit::ButtonSwitch( "Reduce size", &_reduce_size);
|
||||
|
||||
if (transcoder == nullptr)
|
||||
{
|
||||
if (ImGui::Button(ICON_FA_COG " Re-encode", ImVec2(IMGUI_RIGHT_ALIGN,0))) {
|
||||
transcoder = new Transcoder(mp->filename());
|
||||
TranscoderOptions transcode_options(_optim_kf,
|
||||
_optim_still ? PsyTuning::STILL_IMAGE : PsyTuning::NONE,
|
||||
_reduce_size ? 25 : -1,
|
||||
_no_audio);
|
||||
if (!transcoder->start(transcode_options)) {
|
||||
Log::Warning("Failed to start transcoding: %s", transcoder->error().c_str());
|
||||
delete transcoder;
|
||||
transcoder = nullptr;
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGuiToolkit::HelpToolTip("Re-encode the source video\n"
|
||||
"to optimize backward playback\n"
|
||||
"and/or reduce file size\n"
|
||||
"and/or remove audio track\n"
|
||||
"and/or optimize for still images.\n\n"
|
||||
"The new file will replace the one in the source "
|
||||
"once the transcoding is successfully completed.");
|
||||
}
|
||||
|
||||
if (transcoder != nullptr) {
|
||||
if (transcoder->finished()) {
|
||||
|
||||
if (transcoder->success()) {
|
||||
Log::Notify("Transcoding successful : %s", transcoder->outputFilename().c_str());
|
||||
// reload source with new file
|
||||
s.setPath( transcoder->outputFilename() );
|
||||
info.reset();
|
||||
_transcoding = false;
|
||||
}
|
||||
|
||||
delete transcoder;
|
||||
transcoder = nullptr;
|
||||
}
|
||||
else {
|
||||
float progress = transcoder->progress();
|
||||
ImGui::ProgressBar(progress, ImVec2(IMGUI_RIGHT_ALIGN,0), progress < EPSILON ? "working..." : nullptr);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button( ICON_FA_TIMES " Cancel", ImVec2(0,0))) {
|
||||
transcoder->stop();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
// ImGui::PopStyleColor();
|
||||
}
|
||||
else
|
||||
ImGui::SetCursorPos(botom);
|
||||
|
||||
489
src/Transcoder.cpp
Normal file
489
src/Transcoder.cpp
Normal file
@@ -0,0 +1,489 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2025 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 "Transcoder.h"
|
||||
#include "Log.h"
|
||||
|
||||
#include <sys/stat.h>
|
||||
#include <glib.h>
|
||||
#include <gst/transcoder/gsttranscoder.h>
|
||||
|
||||
Transcoder::Transcoder(const std::string& input_filename)
|
||||
: input_filename_(input_filename)
|
||||
, transcoder_(nullptr)
|
||||
, started_(false)
|
||||
, finished_(false)
|
||||
, success_(false)
|
||||
, duration_(-1)
|
||||
, position_(0)
|
||||
{
|
||||
// Output filename will be generated in start() based on options
|
||||
}
|
||||
|
||||
Transcoder::~Transcoder()
|
||||
{
|
||||
if (transcoder_) {
|
||||
g_object_unref(transcoder_);
|
||||
transcoder_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::string Transcoder::generateOutputFilename(const std::string& input, const TranscoderOptions& options)
|
||||
{
|
||||
// Find the last dot to get extension
|
||||
size_t dot_pos = input.rfind('.');
|
||||
size_t slash_pos = input.rfind('/');
|
||||
|
||||
std::string base;
|
||||
if (dot_pos != std::string::npos && (slash_pos == std::string::npos || dot_pos > slash_pos)) {
|
||||
base = input.substr(0, dot_pos);
|
||||
} else {
|
||||
base = input;
|
||||
}
|
||||
|
||||
// Build suffix based on transcoder options
|
||||
std::string suffix = "";
|
||||
|
||||
// Add force keyframes indicator
|
||||
if (options.force_keyframes) {
|
||||
suffix += "_kf";
|
||||
}
|
||||
|
||||
// Add psy-tune indicator
|
||||
switch (options.psy_tune) {
|
||||
case PsyTuning::FILM:
|
||||
suffix += "_film";
|
||||
break;
|
||||
case PsyTuning::ANIMATION:
|
||||
suffix += "_animation";
|
||||
break;
|
||||
case PsyTuning::GRAIN:
|
||||
suffix += "_grain";
|
||||
break;
|
||||
case PsyTuning::STILL_IMAGE:
|
||||
suffix += "_still";
|
||||
break;
|
||||
case PsyTuning::NONE:
|
||||
default:
|
||||
// No suffix for NONE
|
||||
break;
|
||||
}
|
||||
|
||||
// Add CRF indicator if using CRF mode
|
||||
if (options.crf >= 0 && options.crf <= 51) {
|
||||
suffix += "_crf" + std::to_string(options.crf);
|
||||
}
|
||||
|
||||
// Add no audio indicator
|
||||
if (options.force_no_audio) {
|
||||
suffix += "_noaudio";
|
||||
}
|
||||
|
||||
if (suffix.empty()) {
|
||||
suffix = "_transcoded";
|
||||
}
|
||||
|
||||
// Try output filename with generated suffix
|
||||
std::string output = base + suffix + ".mp4";
|
||||
|
||||
// Check if file exists, if so, add numbers
|
||||
struct stat buffer;
|
||||
int counter = 1;
|
||||
while (stat(output.c_str(), &buffer) == 0) {
|
||||
output = base + suffix + "_" + std::to_string(counter) + ".mp4";
|
||||
counter++;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
bool Transcoder::start(const TranscoderOptions& options)
|
||||
{
|
||||
if (started_) {
|
||||
error_message_ = "Transcoder already started";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Generate output filename based on options
|
||||
output_filename_ = generateOutputFilename(input_filename_, options);
|
||||
|
||||
// Check if input file exists
|
||||
struct stat buffer;
|
||||
if (stat(input_filename_.c_str(), &buffer) != 0) {
|
||||
error_message_ = "Input file does not exist: " + input_filename_;
|
||||
Log::Warning("Transcoder: %s", error_message_.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
Log::Info("Transcoder: Starting transcoding from '%s' to '%s'",
|
||||
input_filename_.c_str(), output_filename_.c_str());
|
||||
if (options.force_keyframes) {
|
||||
Log::Info("Transcoder: Force keyframes enabled");
|
||||
}
|
||||
if (options.psy_tune != PsyTuning::NONE) {
|
||||
Log::Info("Transcoder: Psy-tune mode: %d", static_cast<int>(options.psy_tune));
|
||||
}
|
||||
|
||||
// Discover source video properties to match bitrate
|
||||
gchar *src_uri = gst_filename_to_uri(input_filename_.c_str(), nullptr);
|
||||
if (!src_uri) {
|
||||
error_message_ = "Failed to create URI from filename";
|
||||
Log::Warning("Transcoder: %s", error_message_.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
GstDiscoverer *discoverer = gst_discoverer_new(10 * GST_SECOND, nullptr);
|
||||
if (!discoverer) {
|
||||
error_message_ = "Failed to create discoverer";
|
||||
Log::Warning("Transcoder: %s", error_message_.c_str());
|
||||
g_free(src_uri);
|
||||
return false;
|
||||
}
|
||||
|
||||
GError *discover_error = nullptr;
|
||||
GstDiscovererInfo *disc_info = gst_discoverer_discover_uri(discoverer, src_uri, &discover_error);
|
||||
|
||||
guint source_video_bitrate = 0;
|
||||
guint source_audio_bitrate = 0;
|
||||
bool has_audio = false;
|
||||
GstClockTime duration = GST_CLOCK_TIME_NONE;
|
||||
|
||||
if (disc_info) {
|
||||
GstDiscovererResult result = gst_discoverer_info_get_result(disc_info);
|
||||
if (result != GST_DISCOVERER_OK) {
|
||||
const gchar *result_str = "";
|
||||
switch (result) {
|
||||
case GST_DISCOVERER_URI_INVALID: result_str = "Invalid URI"; break;
|
||||
case GST_DISCOVERER_ERROR: result_str = "Discovery error"; break;
|
||||
case GST_DISCOVERER_TIMEOUT: result_str = "Discovery timeout"; break;
|
||||
case GST_DISCOVERER_BUSY: result_str = "Discoverer busy"; break;
|
||||
case GST_DISCOVERER_MISSING_PLUGINS: result_str = "Missing plugins"; break;
|
||||
default: result_str = "Unknown error"; break;
|
||||
}
|
||||
Log::Warning("Transcoder: Discovery failed: %s", result_str);
|
||||
}
|
||||
|
||||
duration = gst_discoverer_info_get_duration(disc_info);
|
||||
|
||||
// Get video stream info
|
||||
GList *video_streams = gst_discoverer_info_get_video_streams(disc_info);
|
||||
if (video_streams) {
|
||||
GstDiscovererVideoInfo *vinfo = (GstDiscovererVideoInfo*)video_streams->data;
|
||||
source_video_bitrate = gst_discoverer_video_info_get_bitrate(vinfo);
|
||||
if (source_video_bitrate == 0) {
|
||||
source_video_bitrate = gst_discoverer_video_info_get_max_bitrate(vinfo);
|
||||
}
|
||||
gst_discoverer_stream_info_list_free(video_streams);
|
||||
} else {
|
||||
Log::Warning("Transcoder: No video stream detected");
|
||||
}
|
||||
|
||||
// Get audio stream info
|
||||
GList *audio_streams = gst_discoverer_info_get_audio_streams(disc_info);
|
||||
if (audio_streams) {
|
||||
has_audio = true;
|
||||
GstDiscovererAudioInfo *ainfo = (GstDiscovererAudioInfo*)audio_streams->data;
|
||||
source_audio_bitrate = gst_discoverer_audio_info_get_bitrate(ainfo);
|
||||
gst_discoverer_stream_info_list_free(audio_streams);
|
||||
}
|
||||
gst_discoverer_info_unref(disc_info);
|
||||
} else {
|
||||
Log::Warning("Transcoder: Could not get discoverer info");
|
||||
}
|
||||
|
||||
if (discover_error) {
|
||||
Log::Warning("Transcoder: Discovery error: %s", discover_error->message);
|
||||
g_error_free(discover_error);
|
||||
}
|
||||
|
||||
g_object_unref(discoverer);
|
||||
g_free(src_uri);
|
||||
|
||||
// If bitrate not available from metadata, calculate from file size and duration
|
||||
if (source_video_bitrate == 0 && duration != GST_CLOCK_TIME_NONE) {
|
||||
struct stat st;
|
||||
if (stat(input_filename_.c_str(), &st) == 0) {
|
||||
guint64 file_size_bits = st.st_size * 8;
|
||||
double duration_seconds = (double)duration / GST_SECOND;
|
||||
guint total_bitrate = (guint)(file_size_bits / duration_seconds);
|
||||
|
||||
// Subtract audio bitrate to estimate video bitrate
|
||||
source_video_bitrate = total_bitrate - source_audio_bitrate;
|
||||
Log::Info("Transcoder: Calculated video bitrate from file size: %u bps (file: %ld bytes, duration: %.2f sec)",
|
||||
source_video_bitrate, st.st_size, duration_seconds);
|
||||
}
|
||||
}
|
||||
|
||||
// Set target bitrates (use source bitrate or reasonable defaults)
|
||||
guint target_video_bitrate = source_video_bitrate > 0 ? source_video_bitrate : 5000000; // 5 Mbps default (in bps)
|
||||
|
||||
// Apply a quality factor (1.1 = 10% higher to ensure no quality loss)
|
||||
const float quality_factor = 1.1f;
|
||||
target_video_bitrate = (guint)(target_video_bitrate * quality_factor / 1000); // convert to kbps
|
||||
Log::Info("Transcoder: Target video bitrate: %u kbps", target_video_bitrate);
|
||||
|
||||
// Create encoding profile for H.264/AAC in MP4 container
|
||||
// Container: MP4
|
||||
GstEncodingContainerProfile *container_profile =
|
||||
gst_encoding_container_profile_new("mp4-profile",
|
||||
"MP4 container profile",
|
||||
gst_caps_from_string("video/quicktime,variant=iso"),
|
||||
nullptr);
|
||||
|
||||
// Disable automatic profile creation - only use explicitly added profiles
|
||||
gst_encoding_profile_set_allow_dynamic_output(GST_ENCODING_PROFILE(container_profile), FALSE);
|
||||
|
||||
// Video profile: H.264
|
||||
// Create a preset with x264enc to force using that encoder
|
||||
GstElement *x264_preset = gst_element_factory_make("x264enc", "x264preset");
|
||||
if (!x264_preset) {
|
||||
error_message_ = "Failed to create x264enc element";
|
||||
Log::Warning("Transcoder: %s", error_message_.c_str());
|
||||
gst_encoding_profile_unref(GST_ENCODING_PROFILE(container_profile));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Configure x264enc properties
|
||||
g_object_set(x264_preset, "speed-preset", 3, NULL); // medium
|
||||
|
||||
// Use CRF mode if specified, otherwise use bitrate mode
|
||||
if (options.crf >= 0 && options.crf <= 51) {
|
||||
g_object_set(x264_preset, "pass", 4, NULL);
|
||||
g_object_set(x264_preset, "quantizer", options.crf, NULL);
|
||||
Log::Info("Transcoder: Using CRF mode with value: %d", options.crf);
|
||||
} else {
|
||||
g_object_set(x264_preset, "pass", 0, NULL);
|
||||
g_object_set(x264_preset, "bitrate", target_video_bitrate, NULL); // kbps
|
||||
Log::Info("Transcoder: Using bitrate mode: %u kbps", target_video_bitrate);
|
||||
}
|
||||
|
||||
// Configure keyframes
|
||||
if (options.force_keyframes) {
|
||||
g_object_set(x264_preset, "key-int-max", 30, NULL);
|
||||
} else {
|
||||
g_object_set(x264_preset, "key-int-max", 250, NULL);
|
||||
}
|
||||
|
||||
// Configure psy-tune
|
||||
if (options.psy_tune != PsyTuning::NONE) {
|
||||
g_object_set(x264_preset, "psy-tune", static_cast<int>(options.psy_tune), NULL);
|
||||
}
|
||||
|
||||
// Save the preset to filesystem
|
||||
const gchar *preset_name = "vimix_x264_transcoding";
|
||||
|
||||
if (!gst_preset_save_preset(GST_PRESET(x264_preset), preset_name)) {
|
||||
error_message_ = "Failed to save x264enc preset";
|
||||
Log::Warning("Transcoder: %s", error_message_.c_str());
|
||||
}
|
||||
else
|
||||
Log::Info("Transcoder: Created x264enc preset '%s'", preset_name);
|
||||
gst_object_unref(x264_preset);
|
||||
|
||||
// Create video profile using the saved preset
|
||||
GstCaps *video_caps = gst_caps_from_string("video/x-h264,profile=main");
|
||||
GstEncodingVideoProfile *video_profile =
|
||||
gst_encoding_video_profile_new(video_caps, preset_name, nullptr, 0);
|
||||
|
||||
// Set video profile presence to always encode
|
||||
gst_encoding_profile_set_presence(GST_ENCODING_PROFILE(video_profile), 1);
|
||||
|
||||
// Add video profiles to container
|
||||
gst_encoding_container_profile_add_profile(container_profile,
|
||||
GST_ENCODING_PROFILE(video_profile));
|
||||
|
||||
gst_caps_unref(video_caps);
|
||||
|
||||
// Handle audio encoding based on source and options
|
||||
if (has_audio) {
|
||||
if (!options.force_no_audio) {
|
||||
// Use detected bitrate or default to 128 kbps
|
||||
guint target_audio_bitrate = source_audio_bitrate > 0 ? source_audio_bitrate / 1000 : 128;
|
||||
Log::Info("Transcoder: Audio stream detected, target bitrate: %u kbps", target_audio_bitrate);
|
||||
|
||||
// Audio profile: AAC
|
||||
GstCaps *audio_caps = gst_caps_from_string("audio/mpeg,mpegversion=4,stream-format=raw");
|
||||
|
||||
// Create audio profile with bitrate
|
||||
GstEncodingAudioProfile *audio_profile =
|
||||
gst_encoding_audio_profile_new(audio_caps, nullptr, nullptr, 0);
|
||||
|
||||
// Set audio bitrate on the profile
|
||||
gst_encoding_profile_set_presence(GST_ENCODING_PROFILE(audio_profile), 1); // encode audio
|
||||
|
||||
// Configure audio encoder properties (for avenc_aac)
|
||||
GstStructure *audio_element_props = gst_structure_new_empty("audio-properties");
|
||||
gst_structure_set(audio_element_props, "bitrate", G_TYPE_INT, target_audio_bitrate * 1000, NULL); // convert to bps
|
||||
gst_encoding_profile_set_element_properties(GST_ENCODING_PROFILE(audio_profile),
|
||||
audio_element_props);
|
||||
// Add audio profile to container
|
||||
gst_encoding_container_profile_add_profile(container_profile,
|
||||
GST_ENCODING_PROFILE(audio_profile));
|
||||
gst_caps_unref(audio_caps);
|
||||
}
|
||||
else {
|
||||
// Add audio profile with presence=0 to explicitly skip audio encoding
|
||||
Log::Info("Transcoder: Audio removal forced by options");
|
||||
}
|
||||
}
|
||||
|
||||
// Create transcoder with the encoding profile
|
||||
src_uri = gst_filename_to_uri(input_filename_.c_str(), nullptr);
|
||||
gchar *dest_uri = gst_filename_to_uri(output_filename_.c_str(), nullptr);
|
||||
|
||||
if (!src_uri || !dest_uri) {
|
||||
error_message_ = "Failed to create URIs from filenames";
|
||||
Log::Warning("Transcoder: %s", error_message_.c_str());
|
||||
g_free(src_uri);
|
||||
g_free(dest_uri);
|
||||
gst_encoding_profile_unref(GST_ENCODING_PROFILE(container_profile));
|
||||
return false;
|
||||
}
|
||||
|
||||
GstTranscoder *transcoder = gst_transcoder_new_full(src_uri, dest_uri,
|
||||
GST_ENCODING_PROFILE(container_profile));
|
||||
g_free(src_uri);
|
||||
g_free(dest_uri);
|
||||
|
||||
if (!transcoder) {
|
||||
error_message_ = "Failed to create GstTranscoder";
|
||||
Log::Warning("Transcoder: %s", error_message_.c_str());
|
||||
gst_encoding_profile_unref(GST_ENCODING_PROFILE(container_profile));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store transcoder for cleanup
|
||||
transcoder_ = G_OBJECT(transcoder);
|
||||
|
||||
// transcoder should try to avoid reencoding streams where reencoding is not strictly needed
|
||||
gst_transcoder_set_avoid_reencoding(transcoder, true);
|
||||
|
||||
// Connect to transcoder signals
|
||||
GstTranscoderSignalAdapter *transcoder_signal = gst_transcoder_get_sync_signal_adapter (transcoder);
|
||||
|
||||
g_signal_connect(transcoder_signal, "done", G_CALLBACK(+[](GstTranscoder *trans, gpointer user_data) {
|
||||
Transcoder *self = static_cast<Transcoder*>(user_data);
|
||||
self->finished_ = true;
|
||||
self->success_ = true;
|
||||
}), this);
|
||||
|
||||
g_signal_connect(transcoder_signal, "error", G_CALLBACK(+[](GstTranscoder *trans, GError *error,
|
||||
GstStructure *details, gpointer user_data) {
|
||||
Transcoder *self = static_cast<Transcoder*>(user_data);
|
||||
self->error_message_ = std::string("Transcoding error: ") + error->message;
|
||||
Log::Warning("Transcoder: %s", self->error_message_.c_str());
|
||||
if (details) {
|
||||
gchar *details_str = gst_structure_to_string(details);
|
||||
Log::Info("Transcoder error details: %s", details_str);
|
||||
g_free(details_str);
|
||||
}
|
||||
self->finished_ = true;
|
||||
self->success_ = false;
|
||||
}), this);
|
||||
|
||||
g_signal_connect(transcoder_signal, "position-updated",
|
||||
G_CALLBACK(+[](GstTranscoder *trans, GstClockTime position, gpointer user_data) {
|
||||
Transcoder *self = static_cast<Transcoder*>(user_data);
|
||||
self->position_ = position;
|
||||
}), this);
|
||||
|
||||
g_signal_connect(transcoder_signal, "duration-changed",
|
||||
G_CALLBACK(+[](GstTranscoder *trans, GstClockTime duration, gpointer user_data) {
|
||||
Transcoder *self = static_cast<Transcoder*>(user_data);
|
||||
self->duration_ = duration;
|
||||
}), this);
|
||||
|
||||
g_signal_connect(transcoder_signal, "warning", G_CALLBACK(+[](GstTranscoder *trans, GError *error,
|
||||
GstStructure *details, gpointer user_data) {
|
||||
Log::Notify("Transcoder warning: %s", error->message);
|
||||
if (details) {
|
||||
gchar *details_str = gst_structure_to_string(details);
|
||||
Log::Info("Warning details: %s", details_str);
|
||||
g_free(details_str);
|
||||
}
|
||||
}), this);
|
||||
|
||||
// Start transcoding
|
||||
gst_transcoder_run_async(transcoder);
|
||||
|
||||
started_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Transcoder::stop()
|
||||
{
|
||||
// Only stop if transcoding is in progress
|
||||
if (!started_ || finished_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (transcoder_) {
|
||||
GstTranscoder *transcoder = GST_TRANSCODER(transcoder_);
|
||||
|
||||
// Get the pipeline from the transcoder
|
||||
GstElement *pipeline = gst_transcoder_get_pipeline(transcoder);
|
||||
if (pipeline) {
|
||||
// Set pipeline to NULL state to stop transcoding
|
||||
gst_element_set_state(pipeline, GST_STATE_NULL);
|
||||
gst_object_unref(pipeline);
|
||||
}
|
||||
|
||||
// Mark as finished (but not successful)
|
||||
finished_ = true;
|
||||
success_ = false;
|
||||
error_message_ = "Transcoding stopped by user";
|
||||
|
||||
Log::Info("Transcoder: Interrupted transcoding");
|
||||
|
||||
// Remove incomplete output file
|
||||
if (!output_filename_.empty()) {
|
||||
struct stat buffer;
|
||||
if (stat(output_filename_.c_str(), &buffer) == 0) {
|
||||
if (remove(output_filename_.c_str()) == 0) {
|
||||
Log::Info("Transcoder: Removed incomplete output file: %s", output_filename_.c_str());
|
||||
} else {
|
||||
Log::Warning("Transcoder: Failed to remove incomplete output file: %s", output_filename_.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Transcoder::finished() const
|
||||
{
|
||||
return finished_;
|
||||
}
|
||||
|
||||
bool Transcoder::success() const
|
||||
{
|
||||
return success_ && finished_;
|
||||
}
|
||||
|
||||
double Transcoder::progress() const
|
||||
{
|
||||
if (!started_ || finished_) {
|
||||
return finished_ ? 1.0 : 0.0;
|
||||
}
|
||||
|
||||
if (duration_ > 0 && position_ >= 0) {
|
||||
return static_cast<double>(position_) / static_cast<double>(duration_);
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
135
src/Transcoder.h
Normal file
135
src/Transcoder.h
Normal file
@@ -0,0 +1,135 @@
|
||||
#ifndef TRANSCODER_H
|
||||
#define TRANSCODER_H
|
||||
|
||||
#include <string>
|
||||
#include <gst/gst.h>
|
||||
#include <gst/pbutils/pbutils.h>
|
||||
|
||||
/**
|
||||
* @brief Psycho-visual tuning options for x264 encoder
|
||||
*/
|
||||
enum class PsyTuning {
|
||||
NONE = 0, ///< No specific tuning
|
||||
FILM = 1, ///< Optimize for film content
|
||||
ANIMATION = 2, ///< Optimize for animation/cartoon content
|
||||
GRAIN = 3, ///< Preserve film grain
|
||||
STILL_IMAGE = 4 ///< Optimize for still image/slideshow content
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Configuration options for transcoding
|
||||
*/
|
||||
struct TranscoderOptions {
|
||||
bool force_keyframes; ///< Force keyframe at every second (for easier seeking/editing)
|
||||
PsyTuning psy_tune; ///< Psycho-visual tuning preset
|
||||
int crf; ///< Constant Rate Factor (0-51, 0=lossless, 23=default, -1=use bitrate mode)
|
||||
bool force_no_audio; ///< Force removal of audio stream (create video-only output)
|
||||
|
||||
/**
|
||||
* @brief Default constructor with sensible defaults
|
||||
*/
|
||||
TranscoderOptions(bool force_keyframes = true
|
||||
, PsyTuning psy_tune = PsyTuning::NONE
|
||||
, int crf = -1
|
||||
, bool force_no_audio = false)
|
||||
: force_keyframes(force_keyframes)
|
||||
, psy_tune(psy_tune)
|
||||
, crf(crf) // -1 = use bitrate mode (default)
|
||||
, force_no_audio(force_no_audio)
|
||||
{}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Video transcoder class using GStreamer
|
||||
*
|
||||
* Encodes video files to H.264/MP4 format using GStreamer's encoding capabilities.
|
||||
* Each instance handles transcoding of a single input file to an output file.
|
||||
*/
|
||||
class Transcoder
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Transcoder
|
||||
* @param input_filename Path to the input video file
|
||||
*
|
||||
* The output filename will be automatically generated in the same folder
|
||||
* with "_transcoded.mp4" suffix, ensuring it doesn't overwrite existing files.
|
||||
*/
|
||||
Transcoder(const std::string& input_filename);
|
||||
|
||||
/**
|
||||
* @brief Destroy the Transcoder and clean up resources
|
||||
*/
|
||||
~Transcoder();
|
||||
|
||||
/**
|
||||
* @brief Start the transcoding process with optional configuration
|
||||
* @param options Transcoding options (keyframes, tuning, etc.)
|
||||
* @return true if transcoding started successfully, false otherwise
|
||||
*/
|
||||
bool start(const TranscoderOptions& options = TranscoderOptions());
|
||||
|
||||
/**
|
||||
* @brief Stop the transcoding process
|
||||
*
|
||||
* Cleanly stops an in-progress transcoding operation and removes the incomplete
|
||||
* output file. If transcoding has already finished or hasn't started, this method
|
||||
* does nothing.
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* @brief Check if transcoding has finished
|
||||
* @return true if transcoding is complete (success or error), false if still running
|
||||
*/
|
||||
bool finished() const;
|
||||
|
||||
/**
|
||||
* @brief Check if transcoding completed successfully
|
||||
* @return true if finished successfully, false if still running or failed
|
||||
*/
|
||||
bool success() const;
|
||||
|
||||
/**
|
||||
* @brief Get the input filename
|
||||
* @return const std::string& Input file path
|
||||
*/
|
||||
const std::string& inputFilename() const { return input_filename_; }
|
||||
|
||||
/**
|
||||
* @brief Get the output filename
|
||||
* @return const std::string& Output file path
|
||||
*/
|
||||
const std::string& outputFilename() const { return output_filename_; }
|
||||
|
||||
/**
|
||||
* @brief Get transcoding progress (0.0 to 1.0)
|
||||
* @return double Progress percentage (0.0 = starting, 1.0 = complete)
|
||||
*/
|
||||
double progress() const;
|
||||
|
||||
/**
|
||||
* @brief Get error message if transcoding failed
|
||||
* @return const std::string& Error message, empty if no error
|
||||
*/
|
||||
const std::string& error() const { return error_message_; }
|
||||
|
||||
private:
|
||||
// Generate output filename from input filename and options
|
||||
std::string generateOutputFilename(const std::string& input, const TranscoderOptions& options);
|
||||
|
||||
std::string input_filename_;
|
||||
std::string output_filename_;
|
||||
std::string error_message_;
|
||||
|
||||
GObject *transcoder_; // GstTranscoder object
|
||||
|
||||
bool started_;
|
||||
bool finished_;
|
||||
bool success_;
|
||||
|
||||
gint64 duration_;
|
||||
gint64 position_;
|
||||
};
|
||||
|
||||
#endif // TRANSCODER_H
|
||||
Reference in New Issue
Block a user