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-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
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
|
Recorder.cpp
|
||||||
Streamer.cpp
|
Streamer.cpp
|
||||||
Loopback.cpp
|
Loopback.cpp
|
||||||
|
Transcoder.cpp
|
||||||
Settings.cpp
|
Settings.cpp
|
||||||
Screenshot.cpp
|
Screenshot.cpp
|
||||||
Resource.cpp
|
Resource.cpp
|
||||||
|
|||||||
@@ -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
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