Add Transcoder class for video transcoding functionality and integrate into GUI

This commit is contained in:
brunoherbelin
2025-11-27 00:34:50 +01:00
parent b0f56d55ed
commit f4ef19e529
6 changed files with 911 additions and 0 deletions

View File

@@ -163,6 +163,7 @@ if (PKG_CONFIG_FOUND)
gstreamer-audio-1.0 gstreamer-audio-1.0
gstreamer-video-1.0 gstreamer-video-1.0
gstreamer-gl-1.0 gstreamer-gl-1.0
gstreamer-transcoder-1.0
) )
include_directories( include_directories(

192
docs/TRANSCODER_USAGE.md Normal file
View 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

View File

@@ -48,6 +48,7 @@ set(VMIX_SRCS
Recorder.cpp Recorder.cpp
Streamer.cpp Streamer.cpp
Loopback.cpp Loopback.cpp
Transcoder.cpp
Settings.cpp Settings.cpp
Screenshot.cpp Screenshot.cpp
Resource.cpp Resource.cpp

View File

@@ -63,6 +63,7 @@
#include "ActionManager.h" #include "ActionManager.h"
#include "Mixer.h" #include "Mixer.h"
#include "ControlManager.h" #include "ControlManager.h"
#include "Transcoder.h"
#include "imgui.h" #include "imgui.h"
#include "ImGuiToolkit.h" #include "ImGuiToolkit.h"
@@ -732,6 +733,98 @@ void ImGuiVisitor::visit (MediaSource& s)
ImGui::TextDisabled("Hardware decoding disabled"); 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 else
ImGui::SetCursorPos(botom); ImGui::SetCursorPos(botom);

489
src/Transcoder.cpp Normal file
View 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
View 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