build: split the engine into liboakengine.so; worker drops the UI entirely

Physical split: app/{audio,cli,codec,common,config,node,pluginSupport,
render,task,timeline,undo,tool,shaders} plus coreengine, version and
ui/icons+colorcoding move to a new top-level engine/ tree, built as
liboakengine.so (shared). The render backends (oakgl/oakvulkan) move
with it and link the engine library instead of embedding a static
render-core subset (libolive-rendercore is gone).

- oak-render-worker now links liboakengine instead of the whole
  libolive-editor object set: 336MB -> 2.9MB, no Qt Widgets UI
- the editor links liboakengine for the engine and keeps only UI
  objects in libolive-editor
- install/packaging: GNUInstallDirs libdir on Linux, bundle copy on
  macOS, oakengine.dll staged for NSIS, AppImage validation entry
- fix backend lookup for the new layout: DynamicRenderer searched
  ../app but backends now live in engine/; a stale pre-split liboakgl
  in the build tree got dlopened instead, re-initialized and later
  destroyed the interposed engine statics (full-suite segfault at
  DialogSequenceParameterTab, found via gdb watchpoint)
This commit is contained in:
2026-07-20 03:23:28 +08:00
parent 026ff94b5e
commit 28c4426236
604 changed files with 243 additions and 172 deletions
+41
View File
@@ -0,0 +1,41 @@
# Olive - Non-Linear Video Editor
# Copyright (C) 2022 Olive Team
#
# 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 <http://www.gnu.org/licenses/>.
add_subdirectory(ffmpeg)
add_subdirectory(oiio)
set(OLIVE_SOURCES
${OLIVE_SOURCES}
codec/conformmanager.cpp
codec/conformmanager.h
codec/decoder.cpp
codec/decoder.h
codec/encoder.cpp
codec/encoder.h
codec/exportcodec.cpp
codec/exportcodec.h
codec/exportformat.cpp
codec/exportformat.h
codec/frame.cpp
codec/frame.h
codec/planarfiledevice.cpp
codec/planarfiledevice.h
codec/proxymanager.cpp
codec/proxymanager.h
codec/timecodemetadata.cpp
codec/timecodemetadata.h
PARENT_SCOPE
)
+158
View File
@@ -0,0 +1,158 @@
/*
* Oak Video Editor - Non-Linear Video Editor
* Copyright (C) 2025 Olive CE Team
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "conformmanager.h"
#include <QDir>
#include "task/taskmanager.h"
namespace olive
{
ConformManager *ConformManager::instance_ = nullptr;
ConformManager::Conform ConformManager::get_conform_state(
const QString &decoder_id, const QString &cache_path,
const Decoder::CodecStream &stream, const AudioParams &params, bool wait)
{
// Mutex because we'll need to check the status of a conform task
QMutexLocker locker(&mutex_);
// Return existing conform if exists
QVector<QString> filenames =
get_conformed_filename(cache_path, stream, params);
if (all_conforms_exist(filenames)) {
return { k_conform_exists, filenames, nullptr };
}
ConformTask *conforming_task = nullptr;
foreach (const ConformData &data, conforming_) {
if (data.stream == stream && data.params == params) {
// Already creating conform in a task
conforming_task = data.task;
break;
}
}
if (!conforming_task) {
// Not conforming yet, create a task to do so
// We conform to a different filename until it's done to make it clear even across sessions
// whether this conform is ready or not
QVector<QString> working_filenames = filenames;
for (int i = 0; i < working_filenames.size(); i++) {
working_filenames[i].append(QStringLiteral(".working"));
}
conforming_task =
new ConformTask(decoder_id, stream, params, working_filenames);
connect(conforming_task, &ConformTask::finished, this,
&ConformManager::conform_task_finished);
conforming_task->moveToThread(TaskManager::instance()->thread());
QMetaObject::invokeMethod(TaskManager::instance(), "add_task",
Qt::QueuedConnection,
Q_ARG(Task *, conforming_task));
conforming_.append(
{ stream, params, conforming_task, working_filenames, filenames });
}
if (wait) {
do {
conform_done_condition_.wait(&mutex_);
} while (!all_conforms_exist(filenames));
return { k_conform_exists, filenames, nullptr };
}
return { k_conform_generating, QVector<QString>(), conforming_task };
}
QVector<QString>
ConformManager::get_conformed_filename(const QString &cache_path,
const Decoder::CodecStream &stream,
const AudioParams &params)
{
QVector<QString> filenames(params.channel_count());
for (int i = 0; i < filenames.size(); i++) {
QString index_fn =
QStringLiteral("%1-%2.%3.%4.%5.%6.pcm")
.arg(FileFunctions::get_unique_file_identifier(stream.filename()),
QString::number(stream.stream()),
QString::number(params.sample_rate()),
QString::number(params.format()),
QString::number(params.channel_layout()),
QString::number(i));
filenames[i] = QDir(cache_path).filePath(index_fn);
}
return filenames;
}
bool ConformManager::all_conforms_exist(const QVector<QString> &filenames)
{
foreach (const QString &fn, filenames) {
if (!QFileInfo::exists(fn)) {
return false;
}
}
return true;
}
void ConformManager::conform_task_finished(Task *task, bool succeeded)
{
QMutexLocker locker(&mutex_);
ConformData data;
// Remove conform data from list
for (int i = 0; i < conforming_.size(); i++) {
const ConformData &c = conforming_.at(i);
if (c.task == task) {
data = c;
conforming_.removeAt(i);
break;
}
}
if (succeeded) {
// Move file to standard conform name, making it clear this conform is ready for use
for (int i = 0; i < data.finished_filename.size(); i++) {
const QString &finished = data.finished_filename.at(i);
const QString &working = data.working_filename.at(i);
QFile::remove(finished);
QFile::rename(working, finished);
}
conform_done_condition_.wakeAll();
locker.unlock();
emit conform_ready();
} else {
// Failed, just delete the working filename if exists
for (int i = 0; i < data.working_filename.size(); i++) {
QFile::remove(data.working_filename.at(i));
}
}
}
}
+108
View File
@@ -0,0 +1,108 @@
/*
* Oak Video Editor - Non-Linear Video Editor
* Copyright (C) 2025 Olive CE Team
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef OAK_CONFORMMANAGER_H
#define OAK_CONFORMMANAGER_H
#include <QMutex>
#include <QObject>
#include "decoder.h"
#include "task/conform/conform.h"
namespace olive
{
class ConformManager : public QObject {
Q_OBJECT
public:
static void create_instance()
{
if (!instance_) {
instance_ = new ConformManager();
}
}
static void destroy_instance()
{
delete instance_;
instance_ = nullptr;
}
static ConformManager *instance()
{
return instance_;
}
enum ConformState { k_conform_exists, k_conform_generating };
struct Conform {
ConformState state;
QVector<QString> filenames;
ConformTask *task;
};
/**
* @brief Get conform state, and start conforming if no conform exists
*
* Thread-safe.
*/
Conform get_conform_state(const QString &decoder_id,
const QString &cache_path,
const Decoder::CodecStream &stream,
const AudioParams &params, bool wait);
signals:
void conform_ready();
private:
ConformManager() = default;
static ConformManager *instance_;
QMutex mutex_;
QWaitCondition conform_done_condition_;
struct ConformData {
Decoder::CodecStream stream;
AudioParams params;
ConformTask *task;
QVector<QString> working_filename;
QVector<QString> finished_filename;
};
QVector<ConformData> conforming_;
/**
* @brief Get the destination filename of an audio stream conformed to a set of parameters
*/
static QVector<QString>
get_conformed_filename(const QString &cache_path,
const Decoder::CodecStream &stream,
const AudioParams &params);
static bool all_conforms_exist(const QVector<QString> &filenames);
private slots:
void conform_task_finished(Task *task, bool succeeded);
};
} // namespace olive
#endif // OAK_CONFORMMANAGER_H
+405
View File
@@ -0,0 +1,405 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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 <http://www.gnu.org/licenses/>.
***/
#include "decoder.h"
#include <QCoreApplication>
#include <QDebug>
#include <QHash>
#include "codec/ffmpeg/ffmpegdecoder.h"
#include "codec/planarfiledevice.h"
#include "codec/oiio/oiiodecoder.h"
#include "conformmanager.h"
namespace olive
{
const Rational Decoder::k_any_timecode = RATIONAL_MIN;
Decoder::Decoder()
: cached_texture_(nullptr)
{
update_last_accessed();
}
void Decoder::increment_access_time(qint64 t)
{
last_accessed_ += t;
}
bool Decoder::open(const CodecStream &stream)
{
QMutexLocker locker(&mutex_);
update_last_accessed();
if (stream_.is_valid()) {
// Decoder is already open. Return TRUE if the stream is the stream we have, or FALSE if not.
if (stream_ == stream) {
return true;
} else {
qWarning()
<< "Tried to open a decoder that was already open with another stream";
return false;
}
} else {
// Stream was not open, try opening it now
if (!stream.is_valid()) {
// Cannot open null stream
qCritical() << "Decoder attempted to open null stream";
return false;
}
if (!stream.exists()) {
// Cannot open file that doesn't exist
qCritical() << "Decoder attempted to open file that doesn't exist";
return false;
}
// Set stream
stream_ = stream;
// Try open internal
if (open_internal()) {
return true;
} else {
// Unset stream
qCritical() << "Failed to open" << stream_.filename() << "stream"
<< stream_.stream();
close_internal();
stream_.reset();
return false;
}
}
}
TexturePtr Decoder::retrieve_video(const RetrieveVideoParams &p)
{
QMutexLocker locker(&mutex_);
update_last_accessed();
if (!stream_.is_valid()) {
qCritical() << "Can't retrieve video on a closed decoder";
return nullptr;
}
if (!supports_video()) {
qCritical() << "Decoder doesn't support video";
return nullptr;
}
if (p.cancelled && p.cancelled->is_cancelled()) {
return nullptr;
}
if (cached_texture_ && cached_time_ == p.time &&
cached_divider_ == p.divider) {
return cached_texture_;
}
cached_texture_ = retrieve_video_internal(p);
cached_time_ = p.time;
cached_divider_ = p.divider;
return cached_texture_;
}
FramePtr Decoder::retrieve_video_frame(const RetrieveVideoParams &p)
{
QMutexLocker locker(&mutex_);
update_last_accessed();
if (!stream_.is_valid()) {
qCritical() << "Can't retrieve video frame on a closed decoder";
return nullptr;
}
if (!supports_video()) {
qCritical() << "Decoder doesn't support video";
return nullptr;
}
if (p.cancelled && p.cancelled->is_cancelled()) {
return nullptr;
}
return retrieve_video_frame_internal(p);
}
Decoder::RetrieveAudioStatus
Decoder::retrieve_audio(SampleBuffer &dest, const TimeRange &range,
const AudioParams &params, const QString &cache_path,
LoopMode loop_mode, RenderMode::Mode mode)
{
QMutexLocker locker(&mutex_);
update_last_accessed();
if (!stream_.is_valid()) {
qCritical() << "Can't retrieve audio on a closed decoder";
return k_invalid;
}
if (!supports_audio()) {
qCritical() << "Decoder doesn't support audio";
return k_invalid;
}
if (params.sample_rate() <= 0 || params.channel_count() <= 0) {
qWarning() << "Invalid audio parameters, skipping audio retrieve";
return k_invalid;
}
// Get conform state from ConformManager
ConformManager::Conform conform =
ConformManager::instance()->get_conform_state(
id(), cache_path, stream_, params, (mode == RenderMode::k_online));
if (conform.state == ConformManager::k_conform_generating) {
// If we need the task, it's available in `conform.task`
return k_waiting_for_conform;
}
// See if we got the conform
if (retrieve_audio_from_conform(dest, conform.filenames, range, loop_mode,
params)) {
return k_ok;
} else {
return k_unknown_error;
}
}
qint64 Decoder::get_last_accessed_time()
{
return last_accessed_;
}
void Decoder::close()
{
QMutexLocker locker(&mutex_);
update_last_accessed();
cached_texture_ = nullptr;
if (stream_.is_valid()) {
close_internal();
stream_.reset();
} else {
qWarning() << "Tried to close a decoder that wasn't open";
}
}
bool Decoder::conform_audio(const QVector<QString> &output_filenames,
const AudioParams &params, CancelAtom *cancelled)
{
return conform_audio_internal(output_filenames, params, cancelled);
}
/*
* DECODER STATIC PUBLIC MEMBERS
*/
QVector<DecoderPtr> Decoder::receive_list_of_all_decoders()
{
QVector<DecoderPtr> decoders;
// The order in which these decoders are added is their priority when probing. Hence FFmpeg should usually be last,
// since it supports so many formats and we presumably want to override those formats with a more specific decoder.
decoders.append(std::make_shared<OIIODecoder>());
decoders.append(std::make_shared<FFmpegDecoder>());
return decoders;
}
DecoderPtr Decoder::create_from_id(const QString &id)
{
if (id.isEmpty()) {
return nullptr;
}
// Create list to iterate through
QVector<DecoderPtr> decoder_list = receive_list_of_all_decoders();
foreach (DecoderPtr d, decoder_list) {
if (d->id() == id) {
return d;
}
}
return nullptr;
}
void Decoder::signal_processing_progress(int64_t ts, int64_t duration)
{
if (duration != FB_NOPTS_VALUE && duration != 0) {
emit index_progress(static_cast<double>(ts) /
static_cast<double>(duration));
}
}
QString Decoder::transform_image_sequence_file_name(const QString &filename,
const int64_t &number)
{
int digit_count = get_image_sequence_digit_count(filename);
QFileInfo file_info(filename);
QString original_basename = file_info.completeBaseName();
QString new_basename =
original_basename.left(original_basename.size() - digit_count)
.append(
QStringLiteral("%1").arg(number, digit_count, 10, QChar('0')));
return file_info.dir().filePath(
file_info.fileName().replace(original_basename, new_basename));
}
int Decoder::get_image_sequence_digit_count(const QString &filename)
{
QString basename = QFileInfo(filename).completeBaseName();
// See if basename contains a number at the end
int digit_count = 0;
for (int i = basename.size() - 1; i >= 0; i--) {
if (basename.at(i).isDigit()) {
digit_count++;
} else {
break;
}
}
return digit_count;
}
int64_t Decoder::get_image_sequence_index(const QString &filename)
{
int digit_count = get_image_sequence_digit_count(filename);
QFileInfo file_info(filename);
QString original_basename = file_info.completeBaseName();
QString number_only =
original_basename.mid(original_basename.size() - digit_count);
return number_only.toLongLong();
}
TexturePtr Decoder::retrieve_video_internal(const RetrieveVideoParams &p)
{
Q_UNUSED(p)
return nullptr;
}
FramePtr Decoder::retrieve_video_frame_internal(const RetrieveVideoParams &p)
{
Q_UNUSED(p)
return nullptr;
}
bool Decoder::conform_audio_internal(const QVector<QString> &filenames,
const AudioParams &params,
CancelAtom *cancelled)
{
Q_UNUSED(filenames)
Q_UNUSED(cancelled)
Q_UNUSED(params)
return false;
}
bool Decoder::retrieve_audio_from_conform(
SampleBuffer &sample_buffer, const QVector<QString> &conform_filenames,
TimeRange range, LoopMode loop_mode, const AudioParams &input_params)
{
PlanarFileDevice input;
if (input.open(conform_filenames, QFile::ReadOnly)) {
// Offset range by audio start offset
range -= get_audio_start_offset();
qint64 read_index = input_params.time_to_bytes(range.in()) /
input_params.channel_count();
qint64 write_index = 0;
const qint64 buffer_length_in_bytes =
sample_buffer.sample_count() *
input_params.bytes_per_sample_per_channel();
while (write_index < buffer_length_in_bytes) {
if (loop_mode == LoopMode::k_loop_mode_loop) {
while (read_index >= input.size()) {
read_index -= input.size();
}
while (read_index < 0) {
read_index += input.size();
}
}
qint64 write_count = 0;
if (read_index < 0) {
// Reading before 0, write silence here until audio data would actually start
write_count = qMin(-read_index, buffer_length_in_bytes);
sample_buffer.silence_bytes(write_index,
write_index + write_count);
} else if (read_index >= input.size()) {
// Reading after data length, write silence until the end of the buffer
write_count = buffer_length_in_bytes - write_index;
sample_buffer.silence_bytes(write_index,
write_index + write_count);
} else {
write_count = qMin(input.size() - read_index,
buffer_length_in_bytes - write_index);
input.seek(read_index);
input.read(reinterpret_cast<char **>(
sample_buffer.to_raw_ptrs().data()),
write_count, write_index);
}
read_index += write_count;
write_index += write_count;
}
input.close();
return true;
}
return false;
}
void Decoder::update_last_accessed()
{
last_accessed_ = QDateTime::currentMSecsSinceEpoch();
}
uint qHash(Decoder::CodecStream stream, uint seed)
{
return qHash(stream.filename(), seed) ^ ::qHash(stream.stream(), seed) ^
qHash(stream.block(), seed);
}
}
+346
View File
@@ -0,0 +1,346 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_DECODER_H
#define OAK_DECODER_H
#include <QFileInfo>
#include <QMutex>
#include <QObject>
#include <QWaitCondition>
#include <stdint.h>
#include "codec/frame.h"
#include "node/block/block.h"
#include "node/project/footage/footagedescription.h"
#include "render/cancelatom.h"
#include "render/rendermodes.h"
namespace olive
{
class Decoder;
using DecoderPtr = std::shared_ptr<Decoder>;
#define DECODER_DEFAULT_DESTRUCTOR(x) \
virtual ~x() override \
{ \
close_internal(); \
}
/**
* @brief A decoder's is the main class for bringing external media into Olive
*
* Its responsibilities are to serve as
* abstraction from codecs/decoders and provide complete frames. These frames can be video or audio data and are
* provided as Frame objects in shared pointers to alleviate the responsibility of memory handling.
*
* The main function in a decoder is Retrieve() which should return complete image/audio data. A decoder should
* alleviate all the complexities of codec compression from the rest of the application (i.e. a decoder should never
* return a partial frame or require other parts of the system to interface directly with the codec). Often this will
* necessitate pre-emptively caching, indexing, or even fully transcoding media before using it which can be implemented
* through the Analyze() function.
*
* A decoder does NOT perform any pixel/sample format conversion. Frames should pass through the PixelService
* to be utilized in the rest of the rendering pipeline.
*/
class Decoder : public QObject {
Q_OBJECT
public:
enum RetrieveState { k_ready, k_failed_to_open, k_index_unavailable };
Decoder();
/**
* @brief Unique decoder ID
*/
virtual QString id() const = 0;
virtual bool supports_video()
{
return false;
}
virtual bool supports_audio()
{
return false;
}
void increment_access_time(qint64 t);
class CodecStream {
public:
CodecStream()
: stream_(-1)
, block_(nullptr)
{
}
CodecStream(const QString &filename, int stream, Block *block)
: filename_(filename)
, stream_(stream)
, block_(block)
{
}
bool is_valid() const
{
return !filename_.isEmpty() && stream_ >= 0;
}
bool exists() const
{
return QFileInfo::exists(filename_);
}
void reset()
{
*this = CodecStream();
}
bool operator==(const CodecStream &rhs) const
{
return filename_ == rhs.filename_ && stream_ == rhs.stream_;
}
const QString &filename() const
{
return filename_;
}
int stream() const
{
return stream_;
}
Block *block() const
{
return block_;
}
private:
QString filename_;
int stream_;
Block *block_;
};
/**
* @brief Open stream for decoding
*
* This function is thread safe.
*
* Returns TRUE if stream could be opened successfully. Also returns TRUE if the decoder is
* already open and the stream == the stream provided. Returns FALSE if the stream couldn't
* be opened OR if already open and the stream is NOT the same.
*/
bool open(const CodecStream &stream);
static const Rational k_any_timecode;
struct RetrieveVideoParams {
Renderer *renderer = nullptr;
Rational time;
int divider = 1;
PixelFormat maximum_format = PixelFormat::invalid;
CancelAtom *cancelled = nullptr;
VideoParams::ColorRange force_range = VideoParams::k_color_range_default;
VideoParams::Interlacing src_interlacing = VideoParams::k_interlace_none;
};
/**
* @brief Retrieves a video frame from footage
*
* This function will always return a valid frame unless a fatal error occurs (in such case,
* nullptr will return). If the timecode is before the start of the footage, this function should
* return the first frame. Likewise, if it is after the timecode, this function should return the
* last frame.
*
* This function is thread safe and can only run while the decoder is open. \see Open()
*/
TexturePtr retrieve_video(const RetrieveVideoParams &p);
/**
* @brief Retrieves a decoded video frame in CPU memory.
*
* Used by render-process isolation to decode media in the main process and pass packed pixel
* data to workers through shared memory.
*/
FramePtr retrieve_video_frame(const RetrieveVideoParams &p);
enum RetrieveAudioStatus {
k_invalid = -1,
k_ok,
k_waiting_for_conform,
k_unknown_error
};
/**
* @brief Retrieve audio data from footage
*
* This function will always return a sample buffer unless a fatal error occurs (in such case,
* nullptr will return). The SampleBuffer should always have enough audio for the range provided.
*
* This function is thread safe and can only run while the decoder is open. \see Open()
*/
RetrieveAudioStatus
retrieve_audio(SampleBuffer &dest, const TimeRange &range,
const AudioParams &params, const QString &cache_path,
LoopMode loop_mode, RenderMode::Mode mode);
/**
* @brief Determine the last time this decoder instance was used in any way
*/
qint64 get_last_accessed_time();
/**
* @brief Generate a Footage object from a file
*
* If this decoder is able to parse this file, it will return a valid FootagePtr. Otherwise, it
* will return nullptr.
*
* For sub-classes, this function should be effectively static. We can't do virtual static
* functions in C++, but it should hold and access no state during its run.
*
* This function is re-entrant.
*/
virtual FootageDescription probe(const QString &filename,
CancelAtom *cancelled) const = 0;
/**
* @brief Closes media/deallocates memory
*
* This function is thread safe and can only run while the decoder is open. \see Open()
*/
void close();
/**
* @brief Conform audio stream
*/
bool conform_audio(const QVector<QString> &output_filenames,
const AudioParams &params,
CancelAtom *cancelled = nullptr);
/**
* @brief Create a Decoder instance using a Decoder ID
*
* @return
*
* A Decoder instance or nullptr if a Decoder with this ID does not exist
*/
static DecoderPtr create_from_id(const QString &id);
static QString transform_image_sequence_file_name(const QString &filename,
const int64_t &number);
static int get_image_sequence_digit_count(const QString &filename);
static int64_t get_image_sequence_index(const QString &filename);
static QVector<DecoderPtr> receive_list_of_all_decoders();
protected:
/**
* @brief Internal open function
*
* Sub-classes must override this function. Function will already be mutexed, so there is no need
* to worry about thread safety. Also many other sanity checks will be done before this, so
* sub-classes only need to worry about their own opening functions. It is guaranteed that the
* decoder is not open yet and that the footage stream was from that sub-classes probe function.
*
* Return TRUE if everything opened successfully and the decoder is ready to work. Otherwise,
* return FALSE. If this function returns false, Decoder will call close_internal to clean any
* memory allocated during OpenInternal.
*/
virtual bool open_internal() = 0;
/**
* @brief Internal close function
*
* Sub-classes must override this function. Function should be able to safely clear all allocated
* memory. It may be called even if Open() didn't complete or RetrieveVideo() was never called.
*/
virtual void close_internal() = 0;
/**
* @brief Internal frame retrieval function
*
* Sub-classes must override this function IF they support video. Function is already mutexed
* so sub-classes don't need to worry about thread safety.
*/
virtual TexturePtr retrieve_video_internal(const RetrieveVideoParams &p);
virtual FramePtr retrieve_video_frame_internal(const RetrieveVideoParams &p);
virtual bool conform_audio_internal(const QVector<QString> &filenames,
const AudioParams &params,
CancelAtom *cancelled);
void signal_processing_progress(int64_t ts, int64_t duration);
/**
* @brief Return currently open stream
*
* This function is NOT thread safe and should therefore only be called by thread safe functions.
*/
const CodecStream &stream() const
{
return stream_;
}
virtual Rational get_audio_start_offset() const
{
return 0;
}
signals:
/**
* @brief While indexing, this signal will provide progress as a percentage (0-100 inclusive) if
* available
*/
void index_progress(double);
private:
void update_last_accessed();
bool retrieve_audio_from_conform(SampleBuffer &sample_buffer,
const QVector<QString> &conform_filenames,
TimeRange range, LoopMode loop_mode,
const AudioParams &params);
CodecStream stream_;
QMutex mutex_;
std::atomic_int64_t last_accessed_;
TexturePtr cached_texture_;
Rational cached_time_;
int cached_divider_;
};
uint qHash(Decoder::CodecStream stream, uint seed = 0);
}
Q_DECLARE_METATYPE(olive::Decoder::RetrieveState)
#endif // OAK_DECODER_H
+590
View File
@@ -0,0 +1,590 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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 <http://www.gnu.org/licenses/>.
***/
#include "encoder.h"
#include <QFile>
#include "common/xmlutils.h"
#include "ffmpeg/ffmpegencoder.h"
#include "oiio/oiioencoder.h"
namespace olive
{
const QRegularExpression Encoder::k_image_sequence_contains_digits =
QRegularExpression(QStringLiteral("\\[[#]+\\]"));
const QRegularExpression Encoder::k_image_sequence_remove_digits =
QRegularExpression(QStringLiteral("[\\-\\.\\ \\_]?\\[[#]+\\]"));
Encoder::Encoder(const EncodingParams &params)
: params_(params)
{
}
const EncodingParams &Encoder::params() const
{
return params_;
}
QString Encoder::get_filename_for_frame(const Rational &frame)
{
if (params().video_is_image_sequence()) {
// Transform!
int64_t frame_index = Timecode::time_to_timestamp(
frame, params().video_params().frame_rate_as_time_base());
int digits = get_image_sequence_placeholder_digit_count(params().filename());
QString frame_index_str =
QStringLiteral("%1").arg(frame_index, digits, 10, QChar('0'));
QString f = params_.filename();
f.replace(k_image_sequence_contains_digits, frame_index_str);
return f;
} else {
// Keep filename
return params_.filename();
}
}
int Encoder::get_image_sequence_placeholder_digit_count(const QString &filename)
{
int start = filename.indexOf(k_image_sequence_contains_digits);
int digit_count = 0;
for (int i = start + 1; i < filename.size(); i++) {
if (filename.at(i) == '#') {
digit_count++;
} else {
break;
}
}
return digit_count;
}
bool Encoder::filename_contains_digit_placeholder(const QString &filename)
{
return filename.contains(k_image_sequence_contains_digits);
}
QString Encoder::filename_remove_digit_placeholder(QString filename)
{
return filename.remove(k_image_sequence_remove_digits);
}
EncodingParams::EncodingParams()
: video_enabled_(false)
, video_bit_rate_(0)
, video_min_bit_rate_(0)
, video_max_bit_rate_(0)
, video_buffer_size_(0)
, video_threads_(0)
, video_is_image_sequence_(false)
, audio_enabled_(false)
, audio_bit_rate_(0)
, subtitles_enabled_(false)
, subtitles_are_sidecar_(false)
, video_scaling_method_(k_stretch)
, has_custom_range_(false)
{
}
QDir EncodingParams::get_preset_path()
{
return QDir(FileFunctions::get_configuration_location())
.filePath(QStringLiteral("exportpresets"));
}
QStringList EncodingParams::get_list_of_presets()
{
QDir d = EncodingParams::get_preset_path();
return d.entryList(QDir::Files);
}
void EncodingParams::enable_video(const VideoParams &video_params,
const ExportCodec::Codec &vcodec)
{
video_enabled_ = true;
video_params_ = video_params;
video_codec_ = vcodec;
}
void EncodingParams::enable_audio(const AudioParams &audio_params,
const ExportCodec::Codec &acodec)
{
audio_enabled_ = true;
audio_params_ = audio_params;
audio_codec_ = acodec;
}
void EncodingParams::enable_subtitles(const ExportCodec::Codec &scodec)
{
subtitles_enabled_ = true;
subtitles_codec_ = scodec;
}
void EncodingParams::enable_sidecar_subtitles(const ExportFormat::Format &sfmt,
const ExportCodec::Codec &scodec)
{
subtitles_enabled_ = true;
subtitles_are_sidecar_ = true;
subtitle_sidecar_fmt_ = sfmt;
subtitles_codec_ = scodec;
}
void EncodingParams::disable_video()
{
video_enabled_ = false;
}
void EncodingParams::disable_audio()
{
audio_enabled_ = false;
}
void EncodingParams::disable_subtitles()
{
subtitles_enabled_ = false;
}
bool EncodingParams::load(QXmlStreamReader *reader)
{
while (xml_read_next_start_element(reader)) {
if (reader->name() == QStringLiteral("export")) {
int version = 0;
XMLAttributeLoop(reader, attr)
{
if (attr.name() == QStringLiteral("version")) {
version = attr.value().toInt();
}
}
switch (version) {
case 1:
return load_v1(reader);
}
} else {
reader->skipCurrentElement();
}
}
return false;
}
bool EncodingParams::load(QIODevice *device)
{
QXmlStreamReader reader(device);
return load(&reader);
}
void EncodingParams::save(QIODevice *device) const
{
QXmlStreamWriter writer(device);
save(&writer);
}
void EncodingParams::save(QXmlStreamWriter *writer) const
{
writer->writeStartDocument();
writer->writeStartElement(QStringLiteral("export"));
writer->writeAttribute(QStringLiteral("version"),
QString::number(k_encoder_params_version));
writer->writeTextElement(QStringLiteral("filename"), filename_);
writer->writeTextElement(QStringLiteral("format"),
QString::number(format_));
writer->writeTextElement(QStringLiteral("range"),
QString::number(has_custom_range_));
writer->writeTextElement(
QStringLiteral("customrangein"),
QString::fromStdString(custom_range_.in().to_string()));
writer->writeTextElement(
QStringLiteral("customrangeout"),
QString::fromStdString(custom_range_.out().to_string()));
writer->writeStartElement(QStringLiteral("video"));
writer->writeAttribute(QStringLiteral("enabled"),
QString::number(video_enabled_));
if (video_enabled_) {
writer->writeTextElement(QStringLiteral("codec"),
QString::number(video_codec_));
writer->writeTextElement(QStringLiteral("width"),
QString::number(video_params_.width()));
writer->writeTextElement(QStringLiteral("height"),
QString::number(video_params_.height()));
writer->writeTextElement(QStringLiteral("format"),
QString::number(video_params_.format()));
writer->writeTextElement(
QStringLiteral("pixelaspect"),
QString::fromStdString(
video_params_.pixel_aspect_ratio().to_string()));
writer->writeTextElement(
QStringLiteral("timebase"),
QString::fromStdString(video_params_.time_base().to_string()));
writer->writeTextElement(QStringLiteral("divider"),
QString::number(video_params_.divider()));
writer->writeTextElement(QStringLiteral("bitrate"),
QString::number(video_bit_rate_));
writer->writeTextElement(QStringLiteral("minbitrate"),
QString::number(video_min_bit_rate_));
writer->writeTextElement(QStringLiteral("maxbitrate"),
QString::number(video_max_bit_rate_));
writer->writeTextElement(QStringLiteral("bufsize"),
QString::number(video_buffer_size_));
writer->writeTextElement(QStringLiteral("threads"),
QString::number(video_threads_));
writer->writeTextElement(QStringLiteral("pixfmt"), video_pix_fmt_);
writer->writeTextElement(QStringLiteral("imgseq"),
QString::number(video_is_image_sequence_));
writer->writeStartElement(QStringLiteral("color"));
writer->writeTextElement(QStringLiteral("output"),
color_transform_.output());
writer->writeEndElement(); // colortransform
writer->writeTextElement(QStringLiteral("vscale"),
QString::number(video_scaling_method_));
if (!video_opts_.isEmpty()) {
writer->writeStartElement(QStringLiteral("opts"));
QHash<QString, QString>::const_iterator i;
for (i = video_opts_.constBegin(); i != video_opts_.constEnd();
i++) {
writer->writeStartElement(QStringLiteral("entry"));
writer->writeTextElement(QStringLiteral("key"), i.key());
writer->writeTextElement(QStringLiteral("value"), i.value());
writer->writeEndElement(); // entry
}
writer->writeEndElement(); // opts
}
}
writer->writeEndElement(); // video
writer->writeStartElement(QStringLiteral("audio"));
writer->writeAttribute(QStringLiteral("enabled"),
QString::number(audio_enabled_));
if (audio_enabled_) {
writer->writeTextElement(QStringLiteral("codec"),
QString::number(audio_codec_));
writer->writeTextElement(QStringLiteral("samplerate"),
QString::number(audio_params_.sample_rate()));
writer->writeTextElement(
QStringLiteral("channellayout"),
QString::number(audio_params().channel_layout()));
writer->writeTextElement(
QStringLiteral("format"),
QString::fromStdString(audio_params_.format().to_string()));
writer->writeTextElement(QStringLiteral("bitrate"),
QString::number(audio_bit_rate_));
}
writer->writeStartElement(QStringLiteral("subtitles"));
writer->writeAttribute(QStringLiteral("enabled"),
QString::number(subtitles_enabled_));
if (subtitles_enabled_) {
writer->writeTextElement(QStringLiteral("sidecar"),
QString::number(subtitles_are_sidecar_));
writer->writeTextElement(QStringLiteral("sidecarformat"),
QString::number(subtitle_sidecar_fmt_));
writer->writeTextElement(QStringLiteral("codec"),
QString::number(subtitles_codec_));
}
writer->writeEndElement(); // subtitles
writer->writeEndElement(); // audio
writer->writeEndElement(); // export
writer->writeEndDocument();
}
Encoder *Encoder::create_from_id(Type id, const EncodingParams &params)
{
switch (id) {
case k_encoder_type_none:
break;
case k_encoder_type_f_fmpeg:
return new FFmpegEncoder(params);
case k_encoder_type_oiio:
return new OIIOEncoder(params);
}
return nullptr;
}
Encoder::Type Encoder::get_type_from_format(ExportFormat::Format f)
{
switch (f) {
case ExportFormat::k_format_d_nx_hd:
case ExportFormat::k_format_matroska:
case ExportFormat::k_format_quick_time:
case ExportFormat::k_format_mpe_g4_video:
case ExportFormat::k_format_mpe_g4_audio:
case ExportFormat::k_format_wav:
case ExportFormat::k_format_aiff:
case ExportFormat::k_format_m_p3:
case ExportFormat::k_format_flac:
case ExportFormat::k_format_ogg:
case ExportFormat::k_format_web_m:
case ExportFormat::k_format_srt:
return k_encoder_type_f_fmpeg;
case ExportFormat::k_format_open_exr:
case ExportFormat::k_format_png:
case ExportFormat::k_format_tiff:
return k_encoder_type_oiio;
case ExportFormat::k_format_count:
break;
}
return k_encoder_type_none;
}
Encoder *Encoder::create_from_format(ExportFormat::Format f,
const EncodingParams &params)
{
return create_from_id(get_type_from_format(f), params);
}
Encoder *Encoder::create_from_params(const EncodingParams &params)
{
return create_from_format(params.format(), params);
}
QStringList Encoder::get_pixel_formats_for_codec(ExportCodec::Codec c) const
{
return QStringList();
}
std::vector<SampleFormat>
Encoder::get_sample_formats_for_codec(ExportCodec::Codec c) const
{
return std::vector<SampleFormat>();
}
QMatrix4x4
EncodingParams::generate_matrix(EncodingParams::VideoScalingMethod method,
int source_width, int source_height,
int dest_width, int dest_height)
{
QMatrix4x4 preview_matrix;
if (method == EncodingParams::k_stretch) {
return preview_matrix;
}
float export_ar =
static_cast<float>(dest_width) / static_cast<float>(dest_height);
float source_ar =
static_cast<float>(source_width) / static_cast<float>(source_height);
if (qFuzzyCompare(export_ar, source_ar)) {
return preview_matrix;
}
if ((export_ar > source_ar) == (method == EncodingParams::k_fit)) {
preview_matrix.scale(source_ar / export_ar, 1.0F);
} else {
preview_matrix.scale(1.0F, export_ar / source_ar);
}
return preview_matrix;
}
bool EncodingParams::load_v1(QXmlStreamReader *reader)
{
Rational custom_range_in, custom_range_out;
while (xml_read_next_start_element(reader)) {
if (reader->name() == QStringLiteral("filename")) {
filename_ = reader->readElementText();
} else if (reader->name() == QStringLiteral("format")) {
format_ = static_cast<ExportFormat::Format>(
reader->readElementText().toInt());
} else if (reader->name() == QStringLiteral("range")) {
has_custom_range_ = reader->readElementText().toInt();
} else if (reader->name() == QStringLiteral("customrangein")) {
custom_range_in =
Rational::from_string(reader->readElementText().toStdString());
} else if (reader->name() == QStringLiteral("customrangeout")) {
custom_range_out =
Rational::from_string(reader->readElementText().toStdString());
} else if (reader->name() == QStringLiteral("video")) {
XMLAttributeLoop(reader, attr)
{
if (attr.name() == QStringLiteral("enabled")) {
video_enabled_ = attr.value().toInt();
}
}
while (xml_read_next_start_element(reader)) {
if (reader->name() == QStringLiteral("codec")) {
video_codec_ = static_cast<ExportCodec::Codec>(
reader->readElementText().toInt());
} else if (reader->name() == QStringLiteral("width")) {
video_params_.set_width(reader->readElementText().toInt());
} else if (reader->name() == QStringLiteral("height")) {
video_params_.set_height(reader->readElementText().toInt());
} else if (reader->name() == QStringLiteral("format")) {
video_params_.set_format(static_cast<PixelFormat::Format>(
reader->readElementText().toInt()));
} else if (reader->name() == QStringLiteral("pixelaspect")) {
video_params_.set_pixel_aspect_ratio(Rational::from_string(
reader->readElementText().toStdString()));
} else if (reader->name() == QStringLiteral("timebase")) {
video_params_.set_time_base(Rational::from_string(
reader->readElementText().toStdString()));
} else if (reader->name() == QStringLiteral("divider")) {
video_params_.set_divider(
reader->readElementText().toInt());
} else if (reader->name() == QStringLiteral("bitrate")) {
video_bit_rate_ = reader->readElementText().toLongLong();
} else if (reader->name() == QStringLiteral("minbitrate")) {
video_min_bit_rate_ =
reader->readElementText().toLongLong();
} else if (reader->name() == QStringLiteral("maxbitrate")) {
video_max_bit_rate_ =
reader->readElementText().toLongLong();
} else if (reader->name() == QStringLiteral("bufsize")) {
video_buffer_size_ = reader->readElementText().toLongLong();
} else if (reader->name() == QStringLiteral("threads")) {
video_threads_ = reader->readElementText().toInt();
} else if (reader->name() == QStringLiteral("pixfmt")) {
video_pix_fmt_ = reader->readElementText();
} else if (reader->name() == QStringLiteral("imgseq")) {
video_is_image_sequence_ =
reader->readElementText().toInt();
} else if (reader->name() == QStringLiteral("color")) {
while (xml_read_next_start_element(reader)) {
if (reader->name() == QStringLiteral("output")) {
color_transform_ = reader->readElementText();
} else {
reader->skipCurrentElement();
}
}
} else if (reader->name() == QStringLiteral("vscale")) {
video_scaling_method_ = static_cast<VideoScalingMethod>(
reader->readElementText().toInt());
} else if (reader->name() == QStringLiteral("opts")) {
while (xml_read_next_start_element(reader)) {
if (reader->name() == QStringLiteral("entry")) {
QString key, value;
while (xml_read_next_start_element(reader)) {
if (reader->name() == QStringLiteral("key")) {
key = reader->readElementText();
} else if (reader->name() ==
QStringLiteral("value")) {
value = reader->readElementText();
} else {
reader->skipCurrentElement();
}
}
set_video_option(key, value);
} else {
reader->skipCurrentElement();
}
}
} else {
reader->skipCurrentElement();
}
}
// HACK: Resolve bug where I forgot to serialize pixel aspect ratio
if (video_params_.pixel_aspect_ratio().isNull()) {
video_params_.set_pixel_aspect_ratio(1);
}
} else if (reader->name() == QStringLiteral("audio")) {
XMLAttributeLoop(reader, attr)
{
if (attr.name() == QStringLiteral("enabled")) {
audio_enabled_ = attr.value().toInt();
}
}
while (xml_read_next_start_element(reader)) {
if (reader->name() == QStringLiteral("codec")) {
audio_codec_ = static_cast<ExportCodec::Codec>(
reader->readElementText().toInt());
} else if (reader->name() == QStringLiteral("samplerate")) {
audio_params_.set_sample_rate(
reader->readElementText().toInt());
} else if (reader->name() == QStringLiteral("channellayout")) {
audio_params_.set_channel_layout(
reader->readElementText().toLongLong());
} else if (reader->name() == QStringLiteral("format")) {
audio_params_.set_format(SampleFormat::from_string(
reader->readElementText().toStdString()));
} else if (reader->name() == QStringLiteral("bitrate")) {
audio_bit_rate_ = reader->readElementText().toLongLong();
} else {
reader->skipCurrentElement();
}
}
// HACK: Resolve bug where I forgot to serialize the audio bit rate
if (!audio_bit_rate_) {
audio_bit_rate_ = 320000;
}
} else if (reader->name() == QStringLiteral("subtitles")) {
XMLAttributeLoop(reader, attr)
{
if (attr.name() == QStringLiteral("enabled")) {
subtitles_enabled_ = attr.value().toInt();
}
}
while (xml_read_next_start_element(reader)) {
if (reader->name() == QStringLiteral("sidecar")) {
subtitles_are_sidecar_ = reader->readElementText().toInt();
} else if (reader->name() == QStringLiteral("sidecarformat")) {
subtitle_sidecar_fmt_ = static_cast<ExportFormat::Format>(
reader->readElementText().toInt());
} else if (reader->name() == QStringLiteral("codec")) {
subtitles_codec_ = static_cast<ExportCodec::Codec>(
reader->readElementText().toInt());
} else {
reader->skipCurrentElement();
}
}
} else {
reader->skipCurrentElement();
}
}
return true;
}
}
+376
View File
@@ -0,0 +1,376 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_ENCODER_H
#define OAK_ENCODER_H
#include <memory>
#include <QRegularExpression>
#include <QString>
#include <QXmlStreamWriter>
#include "codec/exportcodec.h"
#include "codec/exportformat.h"
#include "codec/frame.h"
#include "node/block/subtitle/subtitle.h"
#include "render/colortransform.h"
#include "render/subtitleparams.h"
#include "render/videoparams.h"
//这个代码也许是导出编码视频用的?
namespace olive
{
class Encoder;
using EncoderPtr = std::shared_ptr<Encoder>;
class EncodingParams {
public:
enum VideoScalingMethod { k_fit, k_stretch, k_crop };
EncodingParams();
static QDir get_preset_path();
static QStringList get_list_of_presets();
bool is_valid() const
{
return video_enabled_ || audio_enabled_ || subtitles_enabled_;
}
void set_filename(const QString &filename)
{
filename_ = filename;
}
void enable_video(const VideoParams &video_params,
const ExportCodec::Codec &vcodec);
void enable_audio(const AudioParams &audio_params,
const ExportCodec::Codec &acodec);
void enable_subtitles(const ExportCodec::Codec &scodec);
void enable_sidecar_subtitles(const ExportFormat::Format &sfmt,
const ExportCodec::Codec &scodec);
void disable_video();
void disable_audio();
void disable_subtitles();
const ExportFormat::Format &format() const
{
return format_;
}
void set_format(const ExportFormat::Format &format)
{
format_ = format;
}
void set_video_option(const QString &key, const QString &value)
{
video_opts_.insert(key, value);
}
void set_video_bit_rate(const int64_t &rate)
{
video_bit_rate_ = rate;
}
void set_video_min_bit_rate(const int64_t &rate)
{
video_min_bit_rate_ = rate;
}
void set_video_max_bit_rate(const int64_t &rate)
{
video_max_bit_rate_ = rate;
}
void set_video_buffer_size(const int64_t &sz)
{
video_buffer_size_ = sz;
}
void set_video_threads(const int &threads)
{
video_threads_ = threads;
}
void set_video_pix_fmt(const QString &s)
{
video_pix_fmt_ = s;
}
void set_video_is_image_sequence(bool s)
{
video_is_image_sequence_ = s;
}
void set_color_transform(const ColorTransform &color_transform)
{
color_transform_ = color_transform;
}
const QString &filename() const
{
return filename_;
}
bool video_enabled() const
{
return video_enabled_;
}
const ExportCodec::Codec &video_codec() const
{
return video_codec_;
}
const VideoParams &video_params() const
{
return video_params_;
}
const QHash<QString, QString> &video_opts() const
{
return video_opts_;
}
QString video_option(const QString &key) const
{
return video_opts_.value(key);
}
bool has_video_opt(const QString &key) const
{
return video_opts_.contains(key);
}
const int64_t &video_bit_rate() const
{
return video_bit_rate_;
}
const int64_t &video_min_bit_rate() const
{
return video_min_bit_rate_;
}
const int64_t &video_max_bit_rate() const
{
return video_max_bit_rate_;
}
const int64_t &video_buffer_size() const
{
return video_buffer_size_;
}
const int &video_threads() const
{
return video_threads_;
}
const QString &video_pix_fmt() const
{
return video_pix_fmt_;
}
bool video_is_image_sequence() const
{
return video_is_image_sequence_;
}
const ColorTransform &color_transform() const
{
return color_transform_;
}
bool audio_enabled() const
{
return audio_enabled_;
}
const ExportCodec::Codec &audio_codec() const
{
return audio_codec_;
}
const AudioParams &audio_params() const
{
return audio_params_;
}
const int64_t &audio_bit_rate() const
{
return audio_bit_rate_;
}
void set_audio_bit_rate(const int64_t &b)
{
audio_bit_rate_ = b;
}
bool subtitles_enabled() const
{
return subtitles_enabled_;
}
bool subtitles_are_sidecar() const
{
return subtitles_are_sidecar_;
}
ExportFormat::Format subtitle_sidecar_fmt() const
{
return subtitle_sidecar_fmt_;
}
ExportCodec::Codec subtitles_codec() const
{
return subtitles_codec_;
}
const Rational &get_export_length() const
{
return export_length_;
}
void set_export_length(const Rational &export_length)
{
export_length_ = export_length;
}
bool load(QIODevice *device);
bool load(QXmlStreamReader *reader);
void save(QIODevice *device) const;
void save(QXmlStreamWriter *writer) const;
bool has_custom_range() const
{
return has_custom_range_;
}
const TimeRange &custom_range() const
{
return custom_range_;
}
void set_custom_range(const TimeRange &custom_range)
{
has_custom_range_ = true;
custom_range_ = custom_range;
}
const VideoScalingMethod &video_scaling_method() const
{
return video_scaling_method_;
}
void
set_video_scaling_method(const VideoScalingMethod &video_scaling_method)
{
video_scaling_method_ = video_scaling_method;
}
static QMatrix4x4 generate_matrix(VideoScalingMethod method,
int source_width, int source_height,
int dest_width, int dest_height);
private:
static const int k_encoder_params_version = 1;
bool load_v1(QXmlStreamReader *reader);
QString filename_;
ExportFormat::Format format_ = ExportFormat::k_format_count;
bool video_enabled_;
ExportCodec::Codec video_codec_ = ExportCodec::k_codec_count;
VideoParams video_params_;
QHash<QString, QString> video_opts_;
int64_t video_bit_rate_;
int64_t video_min_bit_rate_;
int64_t video_max_bit_rate_;
int64_t video_buffer_size_;
int video_threads_;
QString video_pix_fmt_;
bool video_is_image_sequence_;
ColorTransform color_transform_;
bool audio_enabled_;
ExportCodec::Codec audio_codec_ = ExportCodec::k_codec_count;
AudioParams audio_params_;
int64_t audio_bit_rate_;
bool subtitles_enabled_;
bool subtitles_are_sidecar_;
ExportFormat::Format subtitle_sidecar_fmt_ = ExportFormat::k_format_count;
ExportCodec::Codec subtitles_codec_ = ExportCodec::k_codec_count;
Rational export_length_;
VideoScalingMethod video_scaling_method_;
bool has_custom_range_;
TimeRange custom_range_;
};
class Encoder : public QObject {
Q_OBJECT
public:
Encoder(const EncodingParams &params);
enum Type { k_encoder_type_none = -1, k_encoder_type_f_fmpeg, k_encoder_type_oiio };
/**
* @brief Create a Encoder instance using a Encoder ID
*
* @return
*
* A Encoder instance or nullptr if a Decoder with this ID does not exist
*/
static Encoder *create_from_id(Type id, const EncodingParams &params);
static Type get_type_from_format(ExportFormat::Format f);
static Encoder *create_from_format(ExportFormat::Format f,
const EncodingParams &params);
static Encoder *create_from_params(const EncodingParams &params);
virtual QStringList get_pixel_formats_for_codec(ExportCodec::Codec c) const;
virtual std::vector<SampleFormat>
get_sample_formats_for_codec(ExportCodec::Codec c) const;
const EncodingParams &params() const;
virtual PixelFormat get_desired_pixel_format() const
{
return PixelFormat::invalid;
}
const QString &get_error() const
{
return error_;
}
QString get_filename_for_frame(const Rational &frame);
static int get_image_sequence_placeholder_digit_count(const QString &filename);
static bool filename_contains_digit_placeholder(const QString &filename);
static QString filename_remove_digit_placeholder(QString filename);
static const QRegularExpression k_image_sequence_contains_digits;
static const QRegularExpression k_image_sequence_remove_digits;
public slots:
virtual bool open() = 0;
virtual bool write_frame(olive::FramePtr frame,
olive::core::Rational time) = 0;
virtual bool write_audio(const olive::SampleBuffer &audio) = 0;
virtual bool write_subtitle(const SubtitleBlock *sub_block) = 0;
virtual void close() = 0;
protected:
void set_error(const QString &err)
{
error_ = err;
}
private:
EncodingParams params_;
QString error_;
};
}
#endif // OAK_ENCODER_H
+139
View File
@@ -0,0 +1,139 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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 <http://www.gnu.org/licenses/>.
***/
#include "exportcodec.h"
extern "C" {
}
namespace olive
{
QString ExportCodec::get_codec_name(ExportCodec::Codec c)
{
switch (c) {
case k_codec_d_nx_hd:
return tr("DNxHD");
case k_codec_h264:
return tr("H.264");
case k_codec_h264rgb:
return tr("H.264 RGB");
case k_codec_h265:
return tr("H.265");
case k_codec_open_exr:
return tr("OpenEXR");
case k_codec_png:
return tr("PNG");
case k_codec_pro_res:
return tr("ProRes");
case k_codec_cineform:
return tr("Cineform");
case k_codec_tiff:
return tr("TIFF");
case k_codec_m_p2:
return tr("MP2");
case k_codec_m_p3:
return tr("MP3");
case k_codec_aac:
return tr("AAC");
case k_codec_pcm:
return tr("PCM (Uncompressed)");
case k_codec_flac:
return tr("FLAC");
case k_codec_opus:
return tr("Opus");
case k_codec_vorbis:
return tr("Vorbis");
case k_codec_v_p9:
return tr("VP9");
case k_codec_a_v1:
return tr("AV1");
case k_codec_srt:
return tr("SubRip SRT");
case k_codec_count:
break;
}
return tr("Unknown");
}
bool ExportCodec::is_codec_a_still_image(ExportCodec::Codec c)
{
switch (c) {
case k_codec_d_nx_hd:
case k_codec_h264:
case k_codec_h264rgb:
case k_codec_h265:
case k_codec_pro_res:
case k_codec_cineform:
case k_codec_m_p2:
case k_codec_m_p3:
case k_codec_aac:
case k_codec_pcm:
case k_codec_vorbis:
case k_codec_opus:
case k_codec_flac:
case k_codec_v_p9:
case k_codec_a_v1:
case k_codec_srt:
return false;
case k_codec_open_exr:
case k_codec_png:
case k_codec_tiff:
return true;
case k_codec_count:
break;
}
return false;
}
bool ExportCodec::is_codec_lossless(Codec c)
{
switch (c) {
case k_codec_pcm:
case k_codec_flac:
return true;
case k_codec_d_nx_hd:
case k_codec_h264:
case k_codec_h264rgb:
case k_codec_h265:
case k_codec_pro_res:
case k_codec_cineform:
case k_codec_m_p2:
case k_codec_m_p3:
case k_codec_aac:
case k_codec_vorbis:
case k_codec_opus:
case k_codec_v_p9:
case k_codec_a_v1:
case k_codec_srt:
case k_codec_open_exr:
case k_codec_png:
case k_codec_tiff:
case k_codec_count:
break;
}
return false;
}
}
+71
View File
@@ -0,0 +1,71 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_EXPORTCODEC_H
#define OAK_EXPORTCODEC_H
#include <QObject>
#include <QString>
#include "common/define.h"
#include "render/subtitleparams.h"
namespace olive
{
class ExportCodec : public QObject {
Q_OBJECT
public:
// Only append to this list (never insert) because indexes are used in serialized files
enum Codec {
k_codec_d_nx_hd,
k_codec_h264,
k_codec_h264rgb,
k_codec_h265,
k_codec_open_exr,
k_codec_png,
k_codec_pro_res,
k_codec_cineform,
k_codec_tiff,
k_codec_v_p9,
k_codec_m_p2,
k_codec_m_p3,
k_codec_aac,
k_codec_pcm,
k_codec_opus,
k_codec_vorbis,
k_codec_flac,
k_codec_srt,
k_codec_a_v1,
k_codec_count
};
static QString get_codec_name(Codec c);
static bool is_codec_a_still_image(Codec c);
static bool is_codec_lossless(Codec c);
};
}
#endif // OAK_EXPORTCODEC_H
+249
View File
@@ -0,0 +1,249 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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 <http://www.gnu.org/licenses/>.
***/
#include "exportformat.h"
#include "encoder.h"
namespace olive
{
QString ExportFormat::get_name(olive::ExportFormat::Format f)
{
switch (f) {
case k_format_d_nx_hd:
return tr("DNxHD");
case k_format_matroska:
return tr("Matroska Video");
case k_format_mpe_g4_video:
return tr("MPEG-4 Video");
case k_format_mpe_g4_audio:
return tr("MPEG-4 Audio");
case k_format_open_exr:
return tr("OpenEXR");
case k_format_png:
return tr("PNG");
case k_format_tiff:
return tr("TIFF");
case k_format_quick_time:
return tr("QuickTime");
case k_format_wav:
return tr("Wave Audio");
case k_format_aiff:
return tr("AIFF");
case k_format_m_p3:
return tr("MP3");
case k_format_flac:
return tr("FLAC");
case k_format_ogg:
return tr("Ogg");
case k_format_web_m:
return tr("WebM");
case k_format_srt:
return tr("SubRip SRT");
case k_format_count:
break;
}
return tr("Unknown");
}
QString ExportFormat::get_extension(ExportFormat::Format f)
{
switch (f) {
case k_format_d_nx_hd:
return QStringLiteral("mxf");
case k_format_matroska:
return QStringLiteral("mkv");
case k_format_mpe_g4_video:
return QStringLiteral("mp4");
case k_format_mpe_g4_audio:
return QStringLiteral("m4a");
case k_format_open_exr:
return QStringLiteral("exr");
case k_format_png:
return QStringLiteral("png");
case k_format_tiff:
return QStringLiteral("tiff");
case k_format_quick_time:
return QStringLiteral("mov");
case k_format_wav:
return QStringLiteral("wav");
case k_format_aiff:
return QStringLiteral("aiff");
case k_format_m_p3:
return QStringLiteral("mp3");
case k_format_flac:
return QStringLiteral("flac");
case k_format_ogg:
return QStringLiteral("ogg");
case k_format_web_m:
return QStringLiteral("webm");
case k_format_srt:
return QStringLiteral("srt");
case k_format_count:
break;
}
return QString();
}
QList<ExportCodec::Codec> ExportFormat::get_video_codecs(ExportFormat::Format f)
{
switch (f) {
case k_format_d_nx_hd:
return { ExportCodec::k_codec_d_nx_hd };
case k_format_matroska:
return { ExportCodec::k_codec_h264, ExportCodec::k_codec_h264rgb,
ExportCodec::k_codec_h265, ExportCodec::k_codec_v_p9 };
case k_format_mpe_g4_video:
return { ExportCodec::k_codec_h264, ExportCodec::k_codec_h264rgb,
ExportCodec::k_codec_h265 };
case k_format_open_exr:
return { ExportCodec::k_codec_open_exr };
case k_format_png:
return { ExportCodec::k_codec_png };
case k_format_tiff:
return { ExportCodec::k_codec_tiff };
case k_format_quick_time:
return { ExportCodec::k_codec_h264, ExportCodec::k_codec_h264rgb,
ExportCodec::k_codec_h265, ExportCodec::k_codec_pro_res,
ExportCodec::k_codec_cineform };
case k_format_web_m:
return { ExportCodec::k_codec_a_v1, ExportCodec::k_codec_v_p9 };
case k_format_ogg:
case k_format_wav:
case k_format_mpe_g4_audio:
case k_format_aiff:
case k_format_m_p3:
case k_format_flac:
case k_format_srt:
case k_format_count:
break;
}
return {};
}
QList<ExportCodec::Codec> ExportFormat::get_audio_codecs(ExportFormat::Format f)
{
switch (f) {
// Video/audio formats
case k_format_d_nx_hd:
return { ExportCodec::k_codec_pcm };
case k_format_matroska:
return { ExportCodec::k_codec_aac, ExportCodec::k_codec_m_p2,
ExportCodec::k_codec_m_p3, ExportCodec::k_codec_pcm,
ExportCodec::k_codec_vorbis, ExportCodec::k_codec_opus,
ExportCodec::k_codec_flac };
case k_format_mpe_g4_video:
case k_format_mpe_g4_audio:
return { ExportCodec::k_codec_aac, ExportCodec::k_codec_m_p2,
ExportCodec::k_codec_m_p3 };
case k_format_quick_time:
return { ExportCodec::k_codec_aac, ExportCodec::k_codec_m_p2,
ExportCodec::k_codec_m_p3, ExportCodec::k_codec_pcm };
case k_format_web_m:
return { ExportCodec::k_codec_opus, ExportCodec::k_codec_aac,
ExportCodec::k_codec_m_p2, ExportCodec::k_codec_m_p3,
ExportCodec::k_codec_pcm, ExportCodec::k_codec_vorbis };
// Audio only formats
case k_format_wav:
return { ExportCodec::k_codec_pcm };
case k_format_aiff:
return { ExportCodec::k_codec_pcm };
case k_format_m_p3:
return { ExportCodec::k_codec_m_p3 };
case k_format_flac:
return { ExportCodec::k_codec_flac };
case k_format_ogg:
return { ExportCodec::k_codec_opus, ExportCodec::k_codec_vorbis,
ExportCodec::k_codec_pcm };
// Video only formats
case k_format_open_exr:
case k_format_png:
case k_format_tiff:
case k_format_srt:
case k_format_count:
break;
}
return {};
}
QList<ExportCodec::Codec> ExportFormat::get_subtitle_codecs(Format f)
{
switch (f) {
case k_format_d_nx_hd:
case k_format_mpe_g4_video:
case k_format_mpe_g4_audio:
case k_format_open_exr:
case k_format_quick_time:
case k_format_png:
case k_format_tiff:
case k_format_wav:
case k_format_aiff:
case k_format_m_p3:
case k_format_flac:
case k_format_ogg:
case k_format_web_m:
case k_format_count:
break;
case k_format_matroska:
case k_format_srt:
return { ExportCodec::k_codec_srt };
}
return {};
}
QStringList ExportFormat::get_pixel_formats_for_codec(ExportFormat::Format f,
ExportCodec::Codec c)
{
Encoder *e = Encoder::create_from_format(f, EncodingParams());
QStringList list;
if (e) {
list = e->get_pixel_formats_for_codec(c);
delete e;
}
return list;
}
std::vector<SampleFormat>
ExportFormat::get_sample_formats_for_codec(Format format, ExportCodec::Codec c)
{
std::vector<SampleFormat> f;
Encoder *e = Encoder::create_from_format(format, EncodingParams());
if (e) {
f = e->get_sample_formats_for_codec(c);
delete e;
}
return f;
}
}
+71
View File
@@ -0,0 +1,71 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_EXPORTFORMAT_H
#define OAK_EXPORTFORMAT_H
#include <QList>
#include <QString>
#include "common/define.h"
#include "exportcodec.h"
namespace olive
{
class ExportFormat : public QObject {
Q_OBJECT
public:
// Only append to this list (never insert) because indexes are used in serialized files
enum Format {
k_format_d_nx_hd,
k_format_matroska,
k_format_mpe_g4_video,
k_format_open_exr,
k_format_quick_time,
k_format_png,
k_format_tiff,
k_format_wav,
k_format_aiff,
k_format_m_p3,
k_format_flac,
k_format_ogg,
k_format_web_m,
k_format_srt,
k_format_mpe_g4_audio,
k_format_count
};
static QString get_name(Format f);
static QString get_extension(Format f);
static QList<ExportCodec::Codec> get_video_codecs(ExportFormat::Format f);
static QList<ExportCodec::Codec> get_audio_codecs(ExportFormat::Format f);
static QList<ExportCodec::Codec> get_subtitle_codecs(ExportFormat::Format f);
static QStringList get_pixel_formats_for_codec(Format f, ExportCodec::Codec c);
static std::vector<SampleFormat>
get_sample_formats_for_codec(Format f, ExportCodec::Codec c);
};
}
#endif // OAK_EXPORTFORMAT_H
+24
View File
@@ -0,0 +1,24 @@
# Olive - Non-Linear Video Editor
# Copyright (C) 2022 Olive Team
#
# 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 <http://www.gnu.org/licenses/>.
set(OLIVE_SOURCES
${OLIVE_SOURCES}
codec/ffmpeg/ffmpegdecoder.cpp
codec/ffmpeg/ffmpegdecoder.h
codec/ffmpeg/ffmpegencoder.cpp
codec/ffmpeg/ffmpegencoder.h
PARENT_SCOPE
)
File diff suppressed because it is too large Load Diff
+151
View File
@@ -0,0 +1,151 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_FFMPEGDECODER_H
#define OAK_FFMPEGDECODER_H
#include <inttypes.h>
#include <ffmpeg_bridge/ffmpeg_bridge.h>
#include <QTimer>
#include <QVector>
#include <QWaitCondition>
#include "codec/decoder.h"
#include "common/ffmpegutils.h"
namespace olive
{
/**
* @brief A Decoder derivative that uses the ffmpeg_bridge library as an Olive decoder
*
* All media access goes through the pure C API of the ffmpeg_bridge shared
* library; this class never sees an FFmpeg structure or function.
*/
class FFmpegDecoder : public Decoder {
Q_OBJECT
public:
// Constructor
FFmpegDecoder();
// Destructor
DECODER_DEFAULT_DESTRUCTOR(FFmpegDecoder)
virtual QString id() const override;
virtual bool supports_video() override
{
return true;
}
virtual bool supports_audio() override
{
return true;
}
virtual FootageDescription probe(const QString &filename,
CancelAtom *cancelled) const override;
protected:
virtual bool open_internal() override;
virtual TexturePtr
retrieve_video_internal(const RetrieveVideoParams &p) override;
virtual FramePtr
retrieve_video_frame_internal(const RetrieveVideoParams &p) override;
virtual bool conform_audio_internal(const QVector<QString> &filenames,
const AudioParams &params,
CancelAtom *cancelled) override;
virtual void close_internal() override;
virtual Rational get_audio_start_offset() const override;
private:
/**
* @brief Handle a bridge error code
*
* Uses the bridge API to retrieve a descriptive string for this error code and sends it to Error(). As such, this
* function also automatically closes the Decoder.
*
* @param error_code
*/
static QString f_fmpeg_error(int error_code);
void free_scaler();
AVFramePtr transfer_hardware_frame(AVFramePtr f);
static PixelFormat get_native_pixel_format(int pix_fmt);
static int get_native_channel_count(int pix_fmt);
static bool is_pixel_format_glsl_compatible(int f);
AVFramePtr get_frame_from_cache(const int64_t &t) const;
void clear_frame_cache();
AVFramePtr pre_process_frame(AVFramePtr f, const RetrieveVideoParams &p);
TexturePtr process_frame_into_texture(AVFramePtr f,
const RetrieveVideoParams &p,
const AVFramePtr original);
AVFramePtr retrieve_frame(const Rational &time, CancelAtom *cancelled);
void remove_first_frame();
static int maximum_queue_size();
FBScaler *scaler_;
int scaler_src_width_;
int scaler_src_height_;
int scaler_src_format_;
int scaler_dst_width_;
int scaler_dst_height_;
int scaler_dst_format_;
int scaler_colrange_;
int scaler_colspace_;
FBPacket *working_packet_;
int64_t second_ts_;
std::list<AVFramePtr> cached_frames_;
bool cache_at_zero_;
bool cache_at_eof_;
FBDecoder *instance_;
// Stream parameters cached on open (the stream object itself lives
// inside the bridge library)
Rational stream_time_base_;
int64_t stream_start_time_;
int64_t stream_duration_;
int64_t format_start_time_;
int input_sample_format_;
int input_sample_rate_;
uint64_t input_channel_layout_mask_;
};
}
#endif // OAK_FFMPEGDECODER_H
+473
View File
@@ -0,0 +1,473 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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 <http://www.gnu.org/licenses/>.
***/
#include "ffmpegencoder.h"
#include <cstring>
#include <vector>
#include <QFile>
#include "common/ffmpegutils.h"
namespace olive
{
FFmpegEncoder::FFmpegEncoder(const EncodingParams &params)
: Encoder(params)
, encoder_(nullptr)
, open_(false)
{
}
bool FFmpegEncoder::get_color_tags_for_colorspace(const QString &colorspace,
int *primaries, int *trc,
int *matrix)
{
const QString name = colorspace.toLower();
if (name.contains(QStringLiteral("pq")) ||
name.contains(QStringLiteral("2084"))) {
*primaries = fb_color_primaries_bt2020;
*trc = fb_color_trc_pq;
*matrix = fb_col_spc_b_t2020_ncl;
return true;
}
if (name.contains(QStringLiteral("hlg"))) {
*primaries = fb_color_primaries_bt2020;
*trc = fb_color_trc_hlg;
*matrix = fb_col_spc_b_t2020_ncl;
return true;
}
if (name.contains(QStringLiteral("2020"))) {
*primaries = fb_color_primaries_bt2020;
*trc = fb_color_trc_bt709;
*matrix = fb_col_spc_b_t2020_ncl;
return true;
}
if (name.contains(QStringLiteral("p3"))) {
*primaries = fb_color_primaries_smpte432;
*trc = fb_color_trc_srgb;
*matrix = fb_col_spc_b_t709;
return true;
}
if (name.contains(QStringLiteral("srgb"))) {
*primaries = fb_color_primaries_bt709;
*trc = fb_color_trc_srgb;
*matrix = fb_col_spc_b_t709;
return true;
}
if (name.contains(QStringLiteral("pal"))) {
*primaries = fb_color_primaries_bt470bg;
*trc = fb_color_trc_gamma28;
*matrix = fb_col_spc_b_t470_bg;
return true;
}
if (name.contains(QStringLiteral("ntsc"))) {
*primaries = fb_color_primaries_smpte170m;
*trc = fb_color_trc_smpte170m;
*matrix = fb_col_spc_smpt_e170_m;
return true;
}
if (name.contains(QStringLiteral("1886")) ||
name.contains(QStringLiteral("709"))) {
*primaries = fb_color_primaries_bt709;
*trc = fb_color_trc_bt709;
*matrix = fb_col_spc_b_t709;
return true;
}
return false;
}
QStringList FFmpegEncoder::get_pixel_formats_for_codec(ExportCodec::Codec c) const
{
QStringList pix_fmts;
int bridge_codec = export_codec_to_bridge(c);
if (bridge_codec != fb_codec_none) {
int count =
fb_encoder_codec_get_pixel_formats(bridge_codec, nullptr, 0);
if (count > 0) {
std::vector<const char *> names(static_cast<size_t>(count));
fb_encoder_codec_get_pixel_formats(bridge_codec, names.data(),
count);
for (int i = 0; i < count; i++) {
pix_fmts.append(QString::fromUtf8(names[size_t(i)]));
}
}
}
return pix_fmts;
}
std::vector<SampleFormat>
FFmpegEncoder::get_sample_formats_for_codec(ExportCodec::Codec c) const
{
std::vector<SampleFormat> f;
if (c == ExportCodec::k_codec_pcm) {
// FFmpeg lists these as separate codecs so we need custom functionality here
// We list signed 16 first because ExportDialog will always use the first element by default
// (because first element is the "default" in FFmpeg)
f = { SampleFormat::s16, SampleFormat::u8, SampleFormat::s32,
SampleFormat::s64, SampleFormat::f32, SampleFormat::f64 };
} else {
int bridge_codec = export_codec_to_bridge(c);
if (bridge_codec != fb_codec_none) {
int count =
fb_encoder_codec_get_sample_formats(bridge_codec, nullptr, 0);
if (count > 0) {
std::vector<int> fmts(static_cast<size_t>(count));
fb_encoder_codec_get_sample_formats(bridge_codec, fmts.data(),
count);
for (int fmt : fmts) {
SampleFormat native =
FFmpegUtils::get_native_sample_format(fmt);
if (native != SampleFormat::invalid) {
f.push_back(native);
}
}
}
}
}
return f;
}
bool FFmpegEncoder::open()
{
if (open_) {
return true;
}
// Convert QString to C string
QByteArray filename_bytes = params().filename().toUtf8();
FBEncoderConfig config;
memset(&config, 0, sizeof(config));
config.filename = filename_bytes.constData();
// Storage keeping C strings alive until fb_encoder_create deep-copies them
QByteArray pix_fmt_bytes;
QByteArray subtitle_header;
std::vector<QByteArray> opt_key_storage;
std::vector<QByteArray> opt_value_storage;
std::vector<const char *> opt_keys;
std::vector<const char *> opt_values;
// Set up video if it's enabled
if (params().video_enabled()) {
config.video_enabled = 1;
config.video_codec = export_codec_to_bridge(params().video_codec());
config.video_width = params().video_params().width();
config.video_height = params().video_params().height();
config.video_pixel_aspect_num =
params().video_params().pixel_aspect_ratio().numerator();
config.video_pixel_aspect_den =
params().video_params().pixel_aspect_ratio().denominator();
config.video_time_base_num =
params().video_params().frame_rate_as_time_base().numerator();
config.video_time_base_den =
params().video_params().frame_rate_as_time_base().denominator();
config.video_frame_rate_num =
params().video_params().frame_rate().numerator();
config.video_frame_rate_den =
params().video_params().frame_rate().denominator();
pix_fmt_bytes = params().video_pix_fmt().toUtf8();
config.video_pix_fmt = pix_fmt_bytes.constData();
// This is the format we will expect frames received in Write() to be in
PixelFormat native_pixel_fmt = params().video_params().format();
// This is the format we will need to convert the frame to for the bridge to understand it
video_conversion_fmt_ =
FFmpegUtils::get_compatible_pixel_format(native_pixel_fmt);
// These are the equivalent pixel formats as bridge pixel formats
int src_alpha_pix_fmt = FFmpegUtils::get_f_fmpeg_pixel_format(
video_conversion_fmt_, VideoParams::k_rgba_channel_count);
int src_noalpha_pix_fmt = FFmpegUtils::get_f_fmpeg_pixel_format(
video_conversion_fmt_, VideoParams::k_rgb_channel_count);
if (src_alpha_pix_fmt == fb_pix_fmt_none ||
src_noalpha_pix_fmt == fb_pix_fmt_none) {
set_error(
tr("Failed to find suitable pixel format for this buffer"));
return false;
}
config.video_src_pix_fmt = src_alpha_pix_fmt;
config.video_color_range =
params().video_params().color_range() ==
VideoParams::k_color_range_full ?
fb_color_range_jpeg :
fb_color_range_mpeg;
switch (params().video_params().interlacing()) {
case VideoParams::k_interlaced_top_first:
config.video_field_order = fb_field_order_tt;
break;
case VideoParams::k_interlaced_bottom_first:
config.video_field_order = fb_field_order_bb;
break;
default:
config.video_field_order = fb_field_order_progressive;
break;
}
config.video_bit_rate = params().video_bit_rate();
config.video_min_bit_rate = params().video_min_bit_rate();
config.video_max_bit_rate = params().video_max_bit_rate();
config.video_buffer_size = params().video_buffer_size();
config.video_threads = params().video_threads();
config.video_color_srgb =
params().color_transform().output().contains(
QStringLiteral("sRGB"), Qt::CaseInsensitive) ?
1 :
0;
// Derive explicit nclc tags (HDR etc.) from the export colorspace;
// the bridge falls back to the legacy sRGB/Rec.709 logic when these
// are unspecified
int color_primaries = fb_color_primaries_unspec;
int color_trc = fb_color_trc_unspec;
int color_matrix = fb_col_spc_unspec;
get_color_tags_for_colorspace(params().color_transform().output(),
&color_primaries, &color_trc,
&color_matrix);
config.video_color_primaries = color_primaries;
config.video_color_trc = color_trc;
config.video_colorspace = color_matrix;
// Custom options (skip Olive-internal keys)
for (auto i = params().video_opts().begin();
i != params().video_opts().end(); i++) {
if (!i.key().startsWith(QStringLiteral("ove_"))) {
opt_key_storage.push_back(i.key().toUtf8());
opt_value_storage.push_back(i.value().toUtf8());
}
}
for (size_t i = 0; i < opt_key_storage.size(); i++) {
opt_keys.push_back(opt_key_storage[i].constData());
opt_values.push_back(opt_value_storage[i].constData());
}
config.video_opt_keys = opt_keys.data();
config.video_opt_values = opt_values.data();
config.video_opt_count = int(opt_keys.size());
}
// Set up audio if it's enabled
if (params().audio_enabled()) {
config.audio_enabled = 1;
config.audio_codec = export_codec_to_bridge(params().audio_codec());
config.audio_sample_rate = params().audio_params().sample_rate();
config.audio_channel_layout_mask =
params().audio_params().channel_layout();
config.audio_sample_format = FFmpegUtils::get_f_fmpeg_sample_format(
params().audio_params().format());
config.audio_bit_rate = params().audio_bit_rate();
}
// Set up subtitles if they're enabled
if (params().subtitles_enabled()) {
config.subtitles_enabled = 1;
config.subtitle_codec = export_codec_to_bridge(params().subtitles_codec());
subtitle_header = SubtitleParams::generate_ass_header().toUtf8();
config.subtitle_header =
reinterpret_cast<const uint8_t *>(subtitle_header.constData());
config.subtitle_header_size = subtitle_header.size();
}
encoder_ = fb_encoder_create(&config);
if (!encoder_) {
set_error(tr("Failed to create encoder"));
return false;
}
if (fb_encoder_open(encoder_) != 0) {
set_error_from_bridge();
fb_encoder_free(&encoder_);
return false;
}
open_ = true;
return true;
}
bool FFmpegEncoder::write_frame(FramePtr frame, Rational time)
{
// We may need to convert this frame to a frame that the bridge will understand
if (frame->format() != video_conversion_fmt_) {
frame = frame->convert(video_conversion_fmt_);
}
int src_pix_fmt = FFmpegUtils::get_f_fmpeg_pixel_format(frame->format(),
frame->channel_count());
int r = fb_encoder_write_video_frame(
encoder_, frame->width(), frame->height(), src_pix_fmt,
reinterpret_cast<const uint8_t *>(frame->data()),
frame->linesize_bytes(), time.to_double());
if (r != 0) {
set_error_from_bridge();
return false;
}
return true;
}
bool FFmpegEncoder::write_audio(const SampleBuffer &audio)
{
if (!audio.is_allocated()) {
return true;
}
const AudioParams &audio_params = audio.audio_params().is_valid() ?
audio.audio_params() :
params().audio_params();
std::vector<const uint8_t *> channel_data(
size_t(audio.audio_params().channel_count()));
for (size_t i = 0; i < channel_data.size(); i++) {
channel_data[i] =
reinterpret_cast<const uint8_t *>(audio.data(int(i)));
}
int r = fb_encoder_write_audio(
encoder_, channel_data.data(),
audio.audio_params().channel_count(),
FFmpegUtils::get_f_fmpeg_sample_format(audio.audio_params().format()),
audio_params.sample_rate(), audio_params.channel_layout(),
int64_t(audio.sample_count()));
if (r != 0) {
set_error_from_bridge();
return false;
}
return true;
}
bool FFmpegEncoder::write_audio_data(const AudioParams &audio_params,
const uint8_t **data,
int input_sample_count)
{
int r = fb_encoder_write_audio(
encoder_, data, audio_params.channel_count(),
FFmpegUtils::get_f_fmpeg_sample_format(audio_params.format()),
audio_params.sample_rate(), audio_params.channel_layout(),
input_sample_count);
if (r != 0) {
set_error_from_bridge();
return false;
}
return true;
}
bool FFmpegEncoder::write_subtitle(const SubtitleBlock *sub_block)
{
QByteArray utf8_sub = sub_block->get_text().toUtf8();
int r = fb_encoder_write_subtitle(encoder_, utf8_sub.constData(),
sub_block->in().to_double(),
sub_block->length().to_double());
if (r != 0) {
set_error_from_bridge();
return false;
}
return true;
}
void FFmpegEncoder::close()
{
if (encoder_) {
// Flushes encoders, writes the trailer, and frees everything
fb_encoder_free(&encoder_);
}
open_ = false;
}
void FFmpegEncoder::set_error_from_bridge()
{
set_error(QString::fromUtf8(fb_encoder_get_error(encoder_)));
}
int FFmpegEncoder::export_codec_to_bridge(ExportCodec::Codec c)
{
switch (c) {
case ExportCodec::k_codec_h264:
return fb_codec_h264;
case ExportCodec::k_codec_h264rgb:
return fb_codec_h264_rgb;
case ExportCodec::k_codec_d_nx_hd:
return fb_codec_dnxhd;
case ExportCodec::k_codec_pro_res:
return fb_codec_prores;
case ExportCodec::k_codec_cineform:
return fb_codec_cineform;
case ExportCodec::k_codec_h265:
return fb_codec_h265;
case ExportCodec::k_codec_v_p9:
return fb_codec_v_p9;
case ExportCodec::k_codec_a_v1:
return fb_codec_a_v1;
case ExportCodec::k_codec_open_exr:
return fb_codec_openexr;
case ExportCodec::k_codec_png:
return fb_codec_png;
case ExportCodec::k_codec_tiff:
return fb_codec_tiff;
case ExportCodec::k_codec_m_p2:
return fb_codec_m_p2;
case ExportCodec::k_codec_m_p3:
return fb_codec_m_p3;
case ExportCodec::k_codec_aac:
return fb_codec_aac;
case ExportCodec::k_codec_pcm:
return fb_codec_pcm;
case ExportCodec::k_codec_flac:
return fb_codec_flac;
case ExportCodec::k_codec_opus:
return fb_codec_opus;
case ExportCodec::k_codec_vorbis:
return fb_codec_vorbis;
case ExportCodec::k_codec_srt:
return fb_codec_srt;
case ExportCodec::k_codec_count:
break;
}
return fb_codec_none;
}
}
+98
View File
@@ -0,0 +1,98 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_FFMPEGENCODER_H
#define OAK_FFMPEGENCODER_H
#include <ffmpeg_bridge/ffmpeg_bridge.h>
#include "codec/encoder.h"
namespace olive
{
/**
* @brief An Encoder derivative that uses the ffmpeg_bridge library for encoding
*
* All encoding work happens inside the ffmpeg_bridge shared library through
* its pure C API; this class only translates EncodingParams into a bridge
* configuration and forwards calls.
*/
class FFmpegEncoder : public Encoder {
Q_OBJECT
public:
FFmpegEncoder(const EncodingParams &params);
virtual QStringList
get_pixel_formats_for_codec(ExportCodec::Codec c) const override;
virtual std::vector<SampleFormat>
get_sample_formats_for_codec(ExportCodec::Codec c) const override;
virtual bool open() override;
virtual bool write_frame(olive::FramePtr frame,
olive::core::Rational time) override;
virtual bool write_audio(const olive::SampleBuffer &audio) override;
bool write_audio_data(const AudioParams &audio_params, const uint8_t **data,
int input_sample_count);
virtual bool write_subtitle(const SubtitleBlock *sub_block) override;
virtual void close() override;
virtual PixelFormat get_desired_pixel_format() const override
{
return video_conversion_fmt_;
}
/**
* @brief Derives nclc color tags from an output colorspace name
*
* Extracted for testability. Returns true when the name maps to
* explicit tags (PQ/HLG/BT.2020, sRGB, P3, Rec.601, Rec.709); returns
* false for unknown names, in which case the bridge's legacy
* Rec.709/sRGB inference applies.
*/
static bool get_color_tags_for_colorspace(const QString &colorspace,
int *primaries, int *trc,
int *matrix);
private:
/**
* @brief Copy the last error message from the bridge into the encoder error state
*/
void set_error_from_bridge();
static int export_codec_to_bridge(ExportCodec::Codec c);
FBEncoder *encoder_;
PixelFormat video_conversion_fmt_;
bool open_;
};
}
#endif // OAK_FFMPEGENCODER_H
+189
View File
@@ -0,0 +1,189 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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 <http://www.gnu.org/licenses/>.
***/
#include "frame.h"
#include <OpenImageIO/imagebuf.h>
#include <QDebug>
#include <QtGlobal>
#include <QtMath>
#include "common/oiioutils.h"
#include "render/framemanager.h"
namespace olive
{
Frame::Frame()
: data_(nullptr)
, data_size_(0)
, timestamp_(0)
{
}
Frame::~Frame()
{
destroy();
}
FramePtr Frame::create()
{
return std::make_shared<Frame>();
}
const VideoParams &Frame::video_params() const
{
return params_;
}
void Frame::set_video_params(const VideoParams &params)
{
params_ = params;
linesize_ = generate_linesize_bytes(width(), params_.format(),
params_.channel_count());
linesize_pixels_ = linesize_ / params_.get_bytes_per_pixel();
}
FramePtr Frame::interlace(FramePtr top, FramePtr bottom)
{
if (top->video_params() != bottom->video_params()) {
qCritical()
<< "Tried to interlace two frames that had incompatible parameters";
return nullptr;
}
FramePtr interlaced = Frame::create();
interlaced->set_video_params(top->video_params());
interlaced->allocate();
int linesize = interlaced->linesize_bytes();
for (int i = 0; i < interlaced->height(); i++) {
FramePtr which = (i % 2 == 0) ? top : bottom;
memcpy(interlaced->data() + i * linesize,
which->const_data() + i * linesize, linesize);
}
return interlaced;
}
int Frame::generate_linesize_bytes(int width, PixelFormat format,
int channel_count)
{
// Align to 32 bytes (not sure if this is necessary?)
return VideoParams::get_bytes_per_pixel(format, channel_count) *
((width + 31) & ~31);
}
Color Frame::get_pixel(int x, int y) const
{
if (!contains_pixel(x, y)) {
return Color();
}
int byte_offset =
y * linesize_bytes() + x * video_params().get_bytes_per_pixel();
return Color(reinterpret_cast<const char *>(data_ + byte_offset),
video_params().format(), video_params().channel_count());
}
bool Frame::contains_pixel(int x, int y) const
{
return (is_allocated() && x >= 0 && x < width() && y >= 0 && y < height());
}
void Frame::set_pixel(int x, int y, const Color &c)
{
if (!contains_pixel(x, y)) {
return;
}
int byte_offset =
y * linesize_bytes() + x * video_params().get_bytes_per_pixel();
c.to_data(reinterpret_cast<char *>(data_ + byte_offset),
video_params().format(), video_params().channel_count());
}
bool Frame::allocate()
{
// Assume this frame is intended to be a video frame
if (!params_.is_valid()) {
qWarning() << "Tried to allocate a frame with invalid parameters";
return false;
}
if (is_allocated()) {
// Already allocated
return true;
}
data_size_ = linesize_ * height();
data_ = FrameManager::allocate(data_size_);
return true;
}
void Frame::destroy()
{
if (is_allocated()) {
FrameManager::deallocate(data_size_, data_);
data_size_ = 0;
data_ = nullptr;
}
}
FramePtr Frame::convert(PixelFormat format) const
{
// Create new params with destination format
VideoParams params = params_;
params.set_format(format);
// Create new frame
FramePtr converted = Frame::create();
converted->set_video_params(params);
converted->set_timestamp(timestamp_);
converted->allocate();
// Do the conversion through OIIO for convenience
OIIO::ImageBuf src(
OIIO::ImageSpec(width(), height(), channel_count(),
OIIOUtils::get_oiio_base_type_from_format(this->format())));
OIIOUtils::frame_to_buffer(this, &src);
OIIO::ImageBuf dst(OIIO::ImageSpec(
converted->width(), converted->height(), channel_count(),
OIIOUtils::get_oiio_base_type_from_format(format)));
if (dst.copy_pixels(src)) {
OIIOUtils::buffer_to_frame(&dst, converted.get());
return converted;
} else {
return nullptr;
}
}
}
+175
View File
@@ -0,0 +1,175 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_FRAME_H
#define OAK_FRAME_H
#include <memory>
#include <olive/core/core.h>
#include <QVector>
#include "common/define.h"
#include "render/videoparams.h"
namespace olive
{
class Frame;
using FramePtr = std::shared_ptr<Frame>;
/**
* @brief Video frame data or audio sample data from a Decoder
*/
class Frame {
public:
Frame();
~Frame();
DISABLE_COPY_MOVE(Frame)
static FramePtr create();
const VideoParams &video_params() const;
void set_video_params(const VideoParams &params);
static FramePtr interlace(FramePtr top, FramePtr bottom);
static int generate_linesize_bytes(int width, PixelFormat format,
int channel_count);
int linesize_pixels() const
{
return linesize_pixels_;
}
int linesize_bytes() const
{
return linesize_;
}
int width() const
{
return params_.effective_width();
}
int height() const
{
return params_.effective_height();
}
PixelFormat format() const
{
return params_.format();
}
int channel_count() const
{
return params_.channel_count();
}
Color get_pixel(int x, int y) const;
bool contains_pixel(int x, int y) const;
void set_pixel(int x, int y, const Color &c);
/**
* @brief Get frame's timestamp.
*
* This timestamp is always a Rational that will equate to the time in seconds.
*/
const Rational &timestamp() const
{
return timestamp_;
}
void set_timestamp(const Rational &timestamp)
{
timestamp_ = timestamp;
}
/**
* @brief Get the data buffer of this frame
*/
char *data()
{
return data_;
}
/**
* @brief Get the const data buffer of this frame
*/
const char *const_data() const
{
return data_;
}
/**
* @brief Allocate memory buffer to store data based on parameters
*
* For video frames, the width(), height(), and format() must be set for this function to work.
*
* If a memory buffer has been previously allocated without destroying, this function will destroy it.
*/
bool allocate();
/**
* @brief Return whether the frame is allocated or not
*/
bool is_allocated() const
{
return data_;
}
/**
* @brief Destroy a memory buffer allocated with allocate()
*/
void destroy();
/**
* @brief Returns the size of the array returned in data() in bytes
*
* Returns 0 if nothing is allocated.
*/
int allocated_size() const
{
return data_size_;
}
FramePtr convert(PixelFormat format) const;
private:
VideoParams params_;
char *data_;
int data_size_;
Rational timestamp_;
int linesize_;
int linesize_pixels_;
};
}
Q_DECLARE_METATYPE(olive::FramePtr)
#endif // OAK_FRAME_H
+24
View File
@@ -0,0 +1,24 @@
# Olive - Non-Linear Video Editor
# Copyright (C) 2022 Olive Team
#
# 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 <http://www.gnu.org/licenses/>.
set(OLIVE_SOURCES
${OLIVE_SOURCES}
codec/oiio/oiiodecoder.cpp
codec/oiio/oiiodecoder.h
codec/oiio/oiioencoder.cpp
codec/oiio/oiioencoder.h
PARENT_SCOPE
)
+288
View File
@@ -0,0 +1,288 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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 <http://www.gnu.org/licenses/>.
***/
#include "oiiodecoder.h"
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include "common/oiioutils.h"
#include "render/renderer.h"
namespace olive
{
QStringList OIIODecoder::supported_formats;
OIIODecoder::OIIODecoder()
: image_(nullptr)
{
}
QString OIIODecoder::id() const
{
return QStringLiteral("oiio");
}
FootageDescription OIIODecoder::probe(const QString &filename,
CancelAtom *cancelled) const
{
Q_UNUSED(cancelled)
FootageDescription desc(id());
// Filter out any file extensions that aren't expected to work - sometimes OIIO will crash trying
// to open a file that it can't if it's given one
if (!file_type_is_supported(filename)) {
return desc;
}
std::string std_filename = filename.toStdString();
auto in = OIIO::ImageInput::open(std_filename);
if (!in) {
return desc;
}
// Filter out OIIO detecting an "FFmpeg movie", we have a native FFmpeg decoder that can handle
// it better
if (!strcmp(in->format_name(), "FFmpeg movie")) {
return desc;
}
bool stream_enabled = true;
int i;
for (i = 0; in->seek_subimage(i, 0); i++) {
OIIO::ImageSpec spec = in->spec();
VideoParams video_params = get_video_params_from_image_spec(spec);
video_params.set_stream_index(i);
if (i > 1) {
// This is a multilayer image and this image might have an offset
OIIO::ImageSpec root_spec = in->spec(0);
float norm_x = spec.x + float(spec.width) * 0.5f -
float(root_spec.width) * 0.5f;
float norm_y = spec.y + float(spec.height) * 0.5f -
float(root_spec.height) * 0.5f;
video_params.set_x(norm_x);
video_params.set_y(norm_y);
}
// By default, only enable the first subimage (presumably the combined image). Later we will
// ask the user if they want to enable the layers instead.
video_params.set_enabled(stream_enabled);
stream_enabled = false;
// OIIO automatically premultiplies alpha
// FIXME: We usually disassociate the alpha for the color management later, for 8-bit images this
// likely reduces the fidelity?
video_params.set_premultiplied_alpha(true);
desc.add_video_stream(video_params);
}
desc.set_stream_count(i);
// If we're here, we have a successful image open
in->close();
return desc;
}
bool OIIODecoder::open_internal()
{
// If we can open the filename provided, assume everything is working
return open_image_handler(stream().filename(), stream().stream());
}
TexturePtr OIIODecoder::retrieve_video_internal(const RetrieveVideoParams &p)
{
FramePtr frame = retrieve_video_frame_internal(p);
if (!frame) {
return nullptr;
}
return p.renderer->create_texture(frame->video_params(), frame->data(),
frame->linesize_pixels());
}
FramePtr OIIODecoder::retrieve_video_frame_internal(const RetrieveVideoParams &p)
{
VideoParams vp = get_video_params_from_image_spec(image_->spec());
vp.set_divider(p.divider);
if (!buffer_.is_allocated() || last_params_.divider != p.divider) {
last_params_ = p;
buffer_.destroy();
buffer_.set_video_params(vp);
buffer_.allocate();
if (p.divider == 1) {
// Just upload straight to the buffer
image_->read_image(0, 0, 0, -1, oiio_pix_fmt_, buffer_.data());
} else {
OIIO::ImageBuf buf(image_->spec());
image_->read_image(0, 0, 0, -1, image_->spec().format,
buf.localpixels(), buf.pixel_stride(),
buf.scanline_stride(), buf.z_stride());
// Roughly downsample image for divider (for some reason OIIO::ImageBufAlgo::resample failed here)
int px_sz = vp.get_bytes_per_pixel();
for (int dst_y = 0; dst_y < buffer_.height(); dst_y++) {
int src_y = dst_y * buf.spec().height / buffer_.height();
for (int dst_x = 0; dst_x < buffer_.width(); dst_x++) {
int src_x = dst_x * buf.spec().width / buffer_.width();
memcpy(buffer_.data() + buffer_.linesize_bytes() * dst_y +
px_sz * dst_x,
static_cast<uint8_t *>(buf.localpixels()) +
buf.scanline_stride() * src_y + px_sz * src_x,
px_sz);
}
}
}
}
// Force F32 output for all still images
if (vp.format() != PixelFormat::f32) {
FramePtr f32_frame = buffer_.convert(PixelFormat::f32);
if (f32_frame) {
f32_frame->set_timestamp(p.time);
return f32_frame;
}
}
FramePtr frame = Frame::create();
frame->set_video_params(buffer_.video_params());
frame->set_timestamp(p.time);
if (!frame->allocate()) {
return nullptr;
}
memcpy(frame->data(), buffer_.const_data(),
size_t(buffer_.allocated_size()));
return frame;
}
void OIIODecoder::close_internal()
{
close_image_handle();
}
bool OIIODecoder::file_type_is_supported(const QString &fn)
{
// We prioritize OIIO over FFmpeg to pick up still images more effectively, but some OIIO decoders (notably OpenJPEG)
// will segfault entirely if given unexpected data (an MPEG-4 for instance). To workaround this issue, we use OIIO's
// "extension_list" attribute and match it with the extension of the file.
// Check if we've created the supported formats list, create it if not
if (supported_formats.isEmpty()) {
QStringList extension_list =
QString::fromStdString(OIIO::get_string_attribute("extension_list"))
.split(';');
// The format of "extension_list" is "format:ext", we want to separate it into a simple list of extensions
foreach (const QString &ext, extension_list) {
QStringList format_and_ext = ext.split(':');
supported_formats.append(format_and_ext.at(1).split(','));
}
}
if (!supported_formats.contains(QFileInfo(fn).suffix(),
Qt::CaseInsensitive)) {
return false;
}
return true;
}
bool OIIODecoder::open_image_handler(const QString &fn, int subimage)
{
image_ = OIIO::ImageInput::open(fn.toStdString());
if (!image_) {
return false;
}
if (!image_->seek_subimage(subimage, 0)) {
return false;
}
// Check if we can work with this pixel format
const OIIO::ImageSpec &spec = image_->spec();
// We use RGBA frames because that tends to be the native format of GPUs
pix_fmt_ = OIIOUtils::get_format_from_oiio_basetype(
static_cast<OIIO::TypeDesc::BASETYPE>(spec.format.basetype));
if (pix_fmt_ == PixelFormat::invalid) {
qWarning()
<< "Failed to convert OIIO::ImageDesc to native pixel format";
return false;
}
oiio_pix_fmt_ = OIIOUtils::get_oiio_base_type_from_format(pix_fmt_);
if (oiio_pix_fmt_ == OIIO::TypeDesc::UNKNOWN) {
qCritical()
<< "Failed to determine appropriate OIIO basetype from native format";
return false;
}
return true;
}
void OIIODecoder::close_image_handle()
{
if (image_) {
image_->close();
image_ = nullptr;
}
buffer_.destroy();
}
VideoParams
OIIODecoder::get_video_params_from_image_spec(const OIIO::ImageSpec &spec)
{
VideoParams video_params;
video_params.set_width(spec.width);
video_params.set_height(spec.height);
video_params.set_format(OIIOUtils::get_format_from_oiio_basetype(
static_cast<OIIO::TypeDesc::BASETYPE>(spec.format.basetype)));
video_params.set_channel_count(spec.nchannels);
video_params.set_pixel_aspect_ratio(
OIIOUtils::get_pixel_aspect_ratio_from_oiio(spec));
video_params.set_video_type(VideoParams::k_video_type_still);
return video_params;
}
}
+80
View File
@@ -0,0 +1,80 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_OIIODECODER_H
#define OAK_OIIODECODER_H
#include <OpenImageIO/imageio.h>
#include <OpenImageIO/imagebuf.h>
#include "codec/decoder.h"
namespace olive
{
class OIIODecoder : public Decoder {
Q_OBJECT
public:
OIIODecoder();
DECODER_DEFAULT_DESTRUCTOR(OIIODecoder)
virtual QString id() const override;
virtual bool supports_video() override
{
return true;
}
virtual FootageDescription probe(const QString &filename,
CancelAtom *cancelled) const override;
protected:
virtual bool open_internal() override;
virtual TexturePtr
retrieve_video_internal(const RetrieveVideoParams &p) override;
virtual FramePtr
retrieve_video_frame_internal(const RetrieveVideoParams &p) override;
virtual void close_internal() override;
private:
std::unique_ptr<OIIO::ImageInput> image_;
static bool file_type_is_supported(const QString &fn);
bool open_image_handler(const QString &fn, int subimage);
void close_image_handle();
static VideoParams get_video_params_from_image_spec(const OIIO::ImageSpec &spec);
PixelFormat pix_fmt_;
OIIO::TypeDesc::BASETYPE oiio_pix_fmt_;
Frame buffer_;
RetrieveVideoParams last_params_;
static QStringList supported_formats;
};
}
#endif // OAK_OIIODECODER_H
+84
View File
@@ -0,0 +1,84 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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 <http://www.gnu.org/licenses/>.
***/
#include "oiioencoder.h"
#include "common/oiioutils.h"
namespace olive
{
OIIOEncoder::OIIOEncoder(const EncodingParams &params)
: Encoder(params)
{
}
bool OIIOEncoder::open()
{
return true;
}
bool OIIOEncoder::write_frame(FramePtr frame, Rational time)
{
std::string filename = get_filename_for_frame(time).toStdString();
auto output = OIIO::ImageOutput::create(filename);
if (!output) {
return false;
}
OIIO::TypeDesc type = OIIOUtils::get_oiio_base_type_from_format(frame->format());
OIIO::ImageSpec spec(frame->width(), frame->height(),
frame->channel_count(), type);
if (!output->open(filename, spec)) {
return false;
}
if (!output->write_image(type, frame->data(), OIIO::AutoStride,
frame->linesize_bytes())) {
return false;
}
if (!output->close()) {
return false;
}
return true;
}
bool OIIOEncoder::write_audio(const SampleBuffer &audio)
{
// Do nothing
return false;
}
bool OIIOEncoder::write_subtitle(const SubtitleBlock *sub_block)
{
return false;
}
void OIIOEncoder::close()
{
// Do nothing
}
}
+48
View File
@@ -0,0 +1,48 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_OIIOENCODER_H
#define OAK_OIIOENCODER_H
#include "codec/encoder.h"
namespace olive
{
class OIIOEncoder : public Encoder {
Q_OBJECT
public:
OIIOEncoder(const EncodingParams &params);
public slots:
virtual bool open() override;
virtual bool write_frame(olive::FramePtr frame,
olive::core::Rational time) override;
virtual bool write_audio(const SampleBuffer &audio) override;
virtual bool write_subtitle(const SubtitleBlock *sub_block) override;
virtual void close() override;
};
}
#endif // OAK_OIIOENCODER_H
+123
View File
@@ -0,0 +1,123 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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 <http://www.gnu.org/licenses/>.
***/
#include "planarfiledevice.h"
namespace olive
{
PlanarFileDevice::PlanarFileDevice(QObject *parent)
: QObject(parent)
{
}
PlanarFileDevice::~PlanarFileDevice()
{
close();
}
bool PlanarFileDevice::open(const QVector<QString> &filenames,
QIODevice::OpenMode mode)
{
if (isOpen()) {
// Already open
return false;
}
files_.resize(filenames.size());
files_.fill(nullptr);
for (int i = 0; i < files_.size(); i++) {
files_[i] = new QFile(filenames.at(i));
if (!files_[i]->open(mode)) {
close();
return false;
}
}
return true;
}
qint64 PlanarFileDevice::read(char **data, qint64 bytes_per_channel,
qint64 offset)
{
qint64 ret = -1;
if (isOpen()) {
for (int i = 0; i < files_.size(); i++) {
// Kind of clunky but should be largely fine
ret = files_[i]->read(data[i] + offset, bytes_per_channel);
}
}
return ret;
}
qint64 PlanarFileDevice::write(const char **data, qint64 bytes_per_channel,
qint64 offset)
{
qint64 ret = -1;
if (isOpen()) {
for (int i = 0; i < files_.size(); i++) {
// Kind of clunky but should be largely fine
ret = files_[i]->write(data[i] + offset, bytes_per_channel);
}
}
return ret;
}
qint64 PlanarFileDevice::size() const
{
if (isOpen()) {
return files_.first()->size();
} else {
return 0;
}
}
bool PlanarFileDevice::seek(qint64 pos)
{
bool ret = true;
for (int i = 0; i < files_.size(); i++) {
ret = files_[i]->seek(pos) & ret;
}
return ret;
}
void PlanarFileDevice::close()
{
for (int i = 0; i < files_.size(); i++) {
QFile *f = files_.at(i);
if (f) {
if (f->isOpen()) {
f->close();
}
delete f;
}
}
files_.clear();
}
}
+65
View File
@@ -0,0 +1,65 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_PLANARFILEDEVICE_H
#define OAK_PLANARFILEDEVICE_H
#include <olive/core/core.h>
#include <QFile>
#include <QObject>
namespace olive
{
using namespace core;
class PlanarFileDevice : public QObject {
Q_OBJECT
public:
PlanarFileDevice(QObject *parent = nullptr);
virtual ~PlanarFileDevice() override;
bool isOpen() const
{
return !files_.isEmpty();
}
bool open(const QVector<QString> &filenames, QIODevice::OpenMode mode);
qint64 read(char **data, qint64 bytes_per_channel, qint64 offset = 0);
qint64 write(const char **data, qint64 bytes_per_channel,
qint64 offset = 0);
qint64 size() const;
bool seek(qint64 pos);
void close();
private:
QVector<QFile *> files_;
};
}
#endif // OAK_PLANARFILEDEVICE_H
+280
View File
@@ -0,0 +1,280 @@
/*
* Oak Video Editor - Non-Linear Video Editor
* Copyright (C) 2026 Oak Team
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "proxymanager.h"
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QMutexLocker>
#include <QStandardPaths>
#include "common/filefunctions.h"
#include "config/config.h"
#include "task/proxy/proxy.h"
#include "task/taskmanager.h"
namespace olive
{
ProxyManager *ProxyManager::instance_ = nullptr;
bool proxy_params_equal(const ProxyManager::ProxyParams &a,
const ProxyManager::ProxyParams &b)
{
return a.width == b.width && a.height == b.height &&
a.divider == b.divider && a.version == b.version &&
a.extension == b.extension && a.crf == b.crf && a.preset == b.preset &&
a.include_audio == b.include_audio;
}
QString ProxyManager::get_proxy_directory(const QString &cache_path)
{
return QDir(cache_path).filePath(QStringLiteral("proxy"));
}
QString ProxyManager::get_proxy_filename(const QString &cache_path,
const QString &source_filename,
int stream_index,
const ProxyParams &params)
{
const QString proxy_dir = get_proxy_directory(cache_path);
const QString extension =
params.extension.isEmpty() ? QStringLiteral("mp4") : params.extension;
// Divider mode scales relative to the source, so the tag names the
// divider rather than an absolute target size
QString size_tag;
if (params.divider > 1) {
size_tag = QStringLiteral("div%1").arg(QString::number(params.divider));
} else {
size_tag = QStringLiteral("%1x%2")
.arg(QString::number(params.width),
QString::number(params.height));
}
const QString filename =
QStringLiteral("%1-%2.%3.v%4.a%5.%6")
.arg(FileFunctions::get_unique_file_identifier(source_filename),
QString::number(stream_index), size_tag,
QString::number(params.version),
params.include_audio ? QStringLiteral("1") : QStringLiteral("0"),
extension);
return QDir(proxy_dir).filePath(filename);
}
QString ProxyManager::get_working_proxy_filename(const QString &proxy_filename)
{
// Append a recognizable suffix while keeping a standard container extension
// so ffmpeg can infer the output format.
return QStringLiteral("%1.working.mp4").arg(proxy_filename);
}
ProxyManager::ProxyState
ProxyManager::get_proxy_state(const QString &proxy_filename)
{
if (QFileInfo::exists(proxy_filename)) {
return k_proxy_ready;
}
if (QFileInfo::exists(get_working_proxy_filename(proxy_filename))) {
return k_proxy_generating;
}
return k_proxy_missing;
}
QString ProxyManager::proxy_state_to_string(ProxyState state)
{
switch (state) {
case k_proxy_missing:
return QStringLiteral("missing");
case k_proxy_generating:
return QStringLiteral("generating");
case k_proxy_ready:
return QStringLiteral("ready");
case k_proxy_failed:
return QStringLiteral("failed");
}
return QStringLiteral("missing");
}
ProxyManager::ProxyState
ProxyManager::proxy_state_from_string(const QString &state)
{
if (state == QStringLiteral("generating")) {
return k_proxy_generating;
}
if (state == QStringLiteral("ready")) {
return k_proxy_ready;
}
if (state == QStringLiteral("failed")) {
return k_proxy_failed;
}
return k_proxy_missing;
}
bool ProxyManager::proxy_filename_has_audio(const QString &proxy_filename)
{
return QFileInfo(proxy_filename).fileName().contains(
QStringLiteral(".a1."));
}
ProxyManager::ProxyParams ProxyManager::proxy_params_from_config()
{
ProxyParams params;
params.width = OAK_CONFIG("ProxyWidth").value<int>();
params.height = OAK_CONFIG("ProxyHeight").value<int>();
params.divider = OAK_CONFIG("ProxyDivider").value<int>();
params.crf = OAK_CONFIG("ProxyCRF").value<int>();
params.preset = OAK_CONFIG("ProxyPreset").toString();
params.include_audio = OAK_CONFIG("ProxyIncludeAudio").toBool();
return params;
}
QString ProxyManager::find_f_fmpeg_executable(const QString &configured_path)
{
// An explicitly configured path takes precedence if it is usable
if (!configured_path.isEmpty()) {
const QFileInfo configured_info(configured_path);
if (configured_info.exists() && configured_info.isFile() &&
configured_info.isExecutable()) {
return configured_info.absoluteFilePath();
}
qWarning() << "Configured ffmpeg path is not a valid executable:"
<< configured_path;
}
// Fall back to searching the system PATH
const QString from_path =
QStandardPaths::findExecutable(QStringLiteral("ffmpeg"));
if (!from_path.isEmpty()) {
return from_path;
}
// Finally, try common install locations (PATH on GUI-launched apps,
// particularly on macOS, often lacks these)
QStringList candidates;
candidates.append(QCoreApplication::applicationDirPath() +
QStringLiteral("/ffmpeg"));
#ifdef Q_OS_MAC
candidates.append(QStringLiteral("/opt/homebrew/bin/ffmpeg"));
candidates.append(QStringLiteral("/usr/local/bin/ffmpeg"));
#endif
#ifdef Q_OS_WINDOWS
candidates.append(QCoreApplication::applicationDirPath() +
QStringLiteral("/ffmpeg.exe"));
#endif
candidates.append(QStringLiteral("/usr/bin/ffmpeg"));
candidates.append(QStringLiteral("/usr/local/bin/ffmpeg"));
for (const QString &candidate : candidates) {
const QFileInfo info(candidate);
if (info.exists() && info.isFile() && info.isExecutable()) {
return info.absoluteFilePath();
}
}
return QString();
}
ProxyManager::Proxy
ProxyManager::get_or_start_proxy(const QString &cache_path,
const QString &source_filename, int stream_index,
const ProxyParams &params)
{
QMutexLocker locker(&mutex_);
const QString filename =
get_proxy_filename(cache_path, source_filename, stream_index, params);
const ProxyState file_state = get_proxy_state(filename);
if (file_state == k_proxy_ready) {
return { k_proxy_ready, filename, nullptr };
}
for (const ProxyData &data : proxying_) {
if (data.source_filename == source_filename &&
data.stream_index == stream_index &&
proxy_params_equal(data.params, params)) {
return { k_proxy_generating, filename, data.task };
}
}
if (file_state == k_proxy_generating) {
QFile::remove(get_working_proxy_filename(filename));
}
const QString working_filename = get_working_proxy_filename(filename);
ProxyTask *task =
new ProxyTask(source_filename, stream_index, params, working_filename);
connect(task, &Task::finished, this, &ProxyManager::proxy_task_finished);
task->moveToThread(TaskManager::instance()->thread());
QMetaObject::invokeMethod(TaskManager::instance(), "add_task",
Qt::QueuedConnection, Q_ARG(Task *, task));
proxying_.append({ source_filename, stream_index, params, task,
working_filename, filename });
return { k_proxy_generating, filename, task };
}
void ProxyManager::proxy_task_finished(Task *task, bool succeeded)
{
QMutexLocker locker(&mutex_);
ProxyData data;
bool found = false;
for (int i = 0; i < proxying_.size(); i++) {
const ProxyData &candidate = proxying_.at(i);
if (candidate.task == task) {
data = candidate;
proxying_.removeAt(i);
found = true;
break;
}
}
if (!found) {
return;
}
if (succeeded) {
QFile::remove(data.finished_filename);
if (QFile::rename(data.working_filename, data.finished_filename)) {
locker.unlock();
emit proxy_ready(data.source_filename, data.stream_index,
data.finished_filename);
emit proxy_finished(data.source_filename, data.stream_index,
data.finished_filename, k_proxy_ready);
return;
}
}
QFile::remove(data.working_filename);
locker.unlock();
emit proxy_finished(data.source_filename, data.stream_index,
data.finished_filename, k_proxy_failed);
}
}
+152
View File
@@ -0,0 +1,152 @@
/*
* Oak Video Editor - Non-Linear Video Editor
* Copyright (C) 2026 Oak Team
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef OAK_PROXYMANAGER_H
#define OAK_PROXYMANAGER_H
#include <QMutex>
#include <QObject>
#include <QString>
#include <QVector>
#include "task/task.h"
namespace olive
{
class ProxyTask;
class ProxyManager : public QObject {
Q_OBJECT
public:
static void create_instance()
{
if (!instance_) {
instance_ = new ProxyManager();
}
}
static void destroy_instance()
{
delete instance_;
instance_ = nullptr;
}
static ProxyManager *instance()
{
return instance_;
}
enum ProxyState {
k_proxy_missing,
k_proxy_generating,
k_proxy_ready,
k_proxy_failed
};
struct ProxyParams {
int width = 1280;
int height = 720;
/**
* @brief Source resolution divider (1 = use absolute width/height,
* 2/4/8 = fraction of the source resolution)
*/
int divider = 1;
int version = 1;
QString extension = QStringLiteral("mp4");
int crf = 23;
QString preset = QStringLiteral("veryfast");
bool include_audio = true;
};
struct Proxy {
ProxyState state = k_proxy_missing;
QString filename;
ProxyTask *task = nullptr;
};
static QString get_proxy_directory(const QString &cache_path);
static QString get_proxy_filename(const QString &cache_path,
const QString &source_filename,
int stream_index,
const ProxyParams &params);
static QString get_working_proxy_filename(const QString &proxy_filename);
static ProxyState get_proxy_state(const QString &proxy_filename);
static QString proxy_state_to_string(ProxyState state);
static ProxyState proxy_state_from_string(const QString &state);
/**
* @brief Returns true if a proxy filename generated by GetProxyFilename()
* indicates the proxy contains audio streams
*/
static bool proxy_filename_has_audio(const QString &proxy_filename);
/**
* @brief Builds proxy parameters from the global application config
*/
static ProxyParams proxy_params_from_config();
/**
* @brief Locates an ffmpeg executable for proxy generation
*
* Resolution order: the explicitly configured path (if non-empty and an
* existing executable file), then the system PATH, then common
* platform-specific install locations. Returns an empty string if no
* executable could be found.
*/
static QString find_f_fmpeg_executable(const QString &configured_path);
Proxy get_or_start_proxy(const QString &cache_path,
const QString &source_filename, int stream_index,
const ProxyParams &params);
signals:
void proxy_ready(const QString &source_filename, int stream_index,
const QString &proxy_filename);
void proxy_finished(const QString &source_filename, int stream_index,
const QString &proxy_filename, ProxyState state);
private:
ProxyManager() = default;
static ProxyManager *instance_;
struct ProxyData {
QString source_filename;
int stream_index = -1;
ProxyParams params;
ProxyTask *task = nullptr;
QString working_filename;
QString finished_filename;
};
QMutex mutex_;
QVector<ProxyData> proxying_;
private slots:
void proxy_task_finished(Task *task, bool succeeded);
};
}
#endif // OAK_PROXYMANAGER_H
+89
View File
@@ -0,0 +1,89 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
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 <http://www.gnu.org/licenses/>.
***/
#include "timecodemetadata.h"
#include <limits>
#include <numeric>
#include "olive/core/util/timecodefunctions.h"
namespace olive
{
TimecodeMetadata::SourceTime
TimecodeMetadata::from_timecode_string(const QString &timecode,
const core::Rational &timebase)
{
SourceTime result;
const QString trimmed = timecode.trimmed();
if (trimmed.isEmpty()) {
return result;
}
bool ok = false;
const core::Timecode::Display display =
trimmed.contains(';') ? core::Timecode::k_timecode_drop_frame :
core::Timecode::k_timecode_non_drop_frame;
result.time = core::Timecode::timecode_to_time(trimmed.toStdString(),
timebase, display, &ok);
result.valid = ok;
if (ok) {
result.source = QStringLiteral("timecode");
}
return result;
}
TimecodeMetadata::SourceTime
TimecodeMetadata::from_bwf_time_reference(const QString &time_reference,
int sample_rate)
{
SourceTime result;
if (sample_rate <= 0) {
return result;
}
bool ok = false;
const qulonglong samples = time_reference.trimmed().toULongLong(&ok);
if (!ok) {
return result;
}
qulonglong numerator = samples;
qulonglong denominator = static_cast<qulonglong>(sample_rate);
const qulonglong divisor = std::gcd(numerator, denominator);
numerator /= divisor;
denominator /= divisor;
const qulonglong rational_limit =
static_cast<qulonglong>(std::numeric_limits<int>::max());
if (numerator <= rational_limit && denominator <= rational_limit) {
result.time = core::Rational(static_cast<int>(numerator),
static_cast<int>(denominator));
} else {
result.time = core::Rational::from_double(
static_cast<double>(samples) / static_cast<double>(sample_rate));
}
result.source = QStringLiteral("bwf_time_reference");
result.valid = true;
return result;
}
}
+48
View File
@@ -0,0 +1,48 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_TIMECODEMETADATA_H
#define OAK_TIMECODEMETADATA_H
#include <QString>
#include "olive/core/util/rational.h"
namespace olive
{
class TimecodeMetadata {
public:
struct SourceTime {
core::Rational time;
QString source;
bool valid = false;
};
static SourceTime from_timecode_string(const QString &timecode,
const core::Rational &timebase);
static SourceTime from_bwf_time_reference(const QString &time_reference,
int sample_rate);
};
}
#endif // OAK_TIMECODEMETADATA_H