Implement proxy media workflow

This commit is contained in:
2026-07-13 10:19:29 +08:00
parent 9c1fba4932
commit 8ae8e3a159
18 changed files with 1671 additions and 5 deletions
+2
View File
@@ -33,6 +33,8 @@ set(OLIVE_SOURCES
codec/frame.h
codec/planarfiledevice.cpp
codec/planarfiledevice.h
codec/proxymanager.cpp
codec/proxymanager.h
codec/timecodemetadata.cpp
codec/timecodemetadata.h
PARENT_SCOPE
+197
View File
@@ -0,0 +1,197 @@
/*
* 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 <QDir>
#include <QFileInfo>
#include <QMutexLocker>
#include "common/filefunctions.h"
#include "task/proxy/proxy.h"
#include "task/taskmanager.h"
namespace olive
{
ProxyManager *ProxyManager::instance_ = nullptr;
bool ProxyParamsEqual(const ProxyManager::ProxyParams &a,
const ProxyManager::ProxyParams &b)
{
return a.width == b.width && a.height == b.height &&
a.version == b.version && a.extension == b.extension;
}
QString ProxyManager::GetProxyDirectory(const QString &cache_path)
{
return QDir(cache_path).filePath(QStringLiteral("proxy"));
}
QString ProxyManager::GetProxyFilename(const QString &cache_path,
const QString &source_filename,
int stream_index,
const ProxyParams &params)
{
const QString proxy_dir = GetProxyDirectory(cache_path);
const QString extension =
params.extension.isEmpty() ? QStringLiteral("mp4") : params.extension;
const QString filename = QStringLiteral("%1-%2.%3x%4.v%5.%6")
.arg(FileFunctions::GetUniqueFileIdentifier(
source_filename),
QString::number(stream_index),
QString::number(params.width),
QString::number(params.height),
QString::number(params.version),
extension);
return QDir(proxy_dir).filePath(filename);
}
QString ProxyManager::GetWorkingProxyFilename(const QString &proxy_filename)
{
return QStringLiteral("%1.working").arg(proxy_filename);
}
ProxyManager::ProxyState
ProxyManager::GetProxyState(const QString &proxy_filename)
{
if (QFileInfo::exists(proxy_filename)) {
return kProxyReady;
}
if (QFileInfo::exists(GetWorkingProxyFilename(proxy_filename))) {
return kProxyGenerating;
}
return kProxyMissing;
}
QString ProxyManager::ProxyStateToString(ProxyState state)
{
switch (state) {
case kProxyMissing:
return QStringLiteral("missing");
case kProxyGenerating:
return QStringLiteral("generating");
case kProxyReady:
return QStringLiteral("ready");
case kProxyFailed:
return QStringLiteral("failed");
}
return QStringLiteral("missing");
}
ProxyManager::ProxyState
ProxyManager::ProxyStateFromString(const QString &state)
{
if (state == QStringLiteral("generating")) {
return kProxyGenerating;
}
if (state == QStringLiteral("ready")) {
return kProxyReady;
}
if (state == QStringLiteral("failed")) {
return kProxyFailed;
}
return kProxyMissing;
}
ProxyManager::Proxy ProxyManager::GetOrStartProxy(
const QString &cache_path, const QString &source_filename,
int stream_index, const ProxyParams &params)
{
QMutexLocker locker(&mutex_);
const QString filename =
GetProxyFilename(cache_path, source_filename, stream_index, params);
const ProxyState file_state = GetProxyState(filename);
if (file_state == kProxyReady) {
return { kProxyReady, filename, nullptr };
}
for (const ProxyData &data : proxying_) {
if (data.source_filename == source_filename &&
data.stream_index == stream_index &&
ProxyParamsEqual(data.params, params)) {
return { kProxyGenerating, filename, data.task };
}
}
if (file_state == kProxyGenerating) {
QFile::remove(GetWorkingProxyFilename(filename));
}
const QString working_filename = GetWorkingProxyFilename(filename);
ProxyTask *task = new ProxyTask(source_filename, stream_index, params,
working_filename);
connect(task, &Task::Finished, this,
&ProxyManager::ProxyTaskFinished);
task->moveToThread(TaskManager::instance()->thread());
QMetaObject::invokeMethod(TaskManager::instance(), "AddTask",
Qt::QueuedConnection, Q_ARG(Task *, task));
proxying_.append({ source_filename, stream_index, params, task,
working_filename, filename });
return { kProxyGenerating, filename, task };
}
void ProxyManager::ProxyTaskFinished(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 ProxyReady(data.source_filename, data.stream_index,
data.finished_filename);
emit ProxyFinished(data.source_filename, data.stream_index,
data.finished_filename, kProxyReady);
return;
}
}
QFile::remove(data.working_filename);
locker.unlock();
emit ProxyFinished(data.source_filename, data.stream_index,
data.finished_filename, kProxyFailed);
}
}
+124
View File
@@ -0,0 +1,124 @@
/*
* 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 PROXYMANAGER_H
#define 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 CreateInstance()
{
if (!instance_) {
instance_ = new ProxyManager();
}
}
static void DestroyInstance()
{
delete instance_;
instance_ = nullptr;
}
static ProxyManager *instance()
{
return instance_;
}
enum ProxyState {
kProxyMissing,
kProxyGenerating,
kProxyReady,
kProxyFailed
};
struct ProxyParams {
int width = 1280;
int height = 720;
int version = 1;
QString extension = QStringLiteral("mp4");
};
struct Proxy {
ProxyState state = kProxyMissing;
QString filename;
ProxyTask *task = nullptr;
};
static QString GetProxyDirectory(const QString &cache_path);
static QString GetProxyFilename(const QString &cache_path,
const QString &source_filename,
int stream_index,
const ProxyParams &params);
static QString GetWorkingProxyFilename(const QString &proxy_filename);
static ProxyState GetProxyState(const QString &proxy_filename);
static QString ProxyStateToString(ProxyState state);
static ProxyState ProxyStateFromString(const QString &state);
Proxy GetOrStartProxy(const QString &cache_path,
const QString &source_filename,
int stream_index,
const ProxyParams &params);
signals:
void ProxyReady(const QString &source_filename, int stream_index,
const QString &proxy_filename);
void ProxyFinished(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 ProxyTaskFinished(Task *task, bool succeeded);
};
}
#endif // PROXYMANAGER_H
+6
View File
@@ -41,6 +41,7 @@
#include "audio/audiomanager.h"
#include "cli/clitask/clitaskdialog.h"
#include "codec/conformmanager.h"
#include "codec/proxymanager.h"
#include "common/filefunctions.h"
#include "common/xmlutils.h"
#include "config/config.h"
@@ -239,6 +240,9 @@ void Core::Start()
// Initialize ConformManager
ConformManager::CreateInstance();
// Initialize ProxyManager
ProxyManager::CreateInstance();
// Initialize RenderManager
RenderManager::CreateInstance();
@@ -296,6 +300,8 @@ void Core::Stop()
ConformManager::DestroyInstance();
ProxyManager::DestroyInstance();
FrameManager::DestroyInstance();
RenderManager::DestroyInstance();
+109
View File
@@ -45,6 +45,10 @@ Footage::Footage(const QString &filename)
: ViewerOutput(false, false)
, timestamp_(0)
, has_source_start_time_(false)
, proxy_enabled_(false)
, proxy_state_(ProxyManager::kProxyMissing)
, proxy_video_stream_index_(-1)
, proxy_preset_version_(0)
, valid_(false)
, cancelled_(nullptr)
, total_stream_count_(0)
@@ -138,6 +142,7 @@ void Footage::Clear()
has_source_start_time_ = false;
source_start_time_ = rational();
source_start_time_source_.clear();
ClearProxy();
// Clear total stream count
total_stream_count_ = 0;
@@ -224,6 +229,28 @@ void Footage::SetSourceStartTime(const rational &time, const QString &source)
has_source_start_time_ = true;
}
void Footage::SetProxy(const QString &path,
ProxyManager::ProxyState state,
int video_stream_index,
int preset_version,
bool enabled)
{
proxy_path_ = path;
proxy_state_ = state;
proxy_video_stream_index_ = video_stream_index;
proxy_preset_version_ = preset_version;
proxy_enabled_ = enabled;
}
void Footage::ClearProxy()
{
proxy_enabled_ = false;
proxy_path_.clear();
proxy_state_ = ProxyManager::kProxyMissing;
proxy_video_stream_index_ = -1;
proxy_preset_version_ = 0;
}
QString Footage::DescribeVideoStream(const VideoParams &params)
{
if (params.video_type() == VideoParams::kVideoTypeStill) {
@@ -275,6 +302,13 @@ void Footage::Value(const NodeValueRow &value, const NodeGlobals &globals,
if (ref.type() == Track::kVideo) {
VideoParams vp = GetVideoParams(ref.index());
if (proxy_enabled_ && !proxy_path_.isEmpty() &&
proxy_video_stream_index_ == vp.stream_index() &&
ProxyManager::GetProxyState(proxy_path_) ==
ProxyManager::kProxyReady) {
job.set_proxy(proxy_path_, QStringLiteral("ffmpeg"), 0);
}
// Ensure the colorspace is valid and not empty
vp.set_colorspace(GetColorspaceToUse(vp));
@@ -473,6 +507,32 @@ bool Footage::LoadCustom(QXmlStreamReader *reader, SerializedData *data)
while (XMLReadNextStartElement(reader)) {
if (reader->name() == QStringLiteral("timestamp")) {
this->set_timestamp(reader->readElementText().toLongLong());
} else if (reader->name() == QStringLiteral("proxy")) {
bool enabled = false;
ProxyManager::ProxyState state = ProxyManager::kProxyMissing;
int stream = -1;
int preset_version = 0;
{
XMLAttributeLoop(reader, attr)
{
if (attr.name() == QStringLiteral("enabled")) {
enabled = (attr.value() == QStringLiteral("1") ||
attr.value() == QStringLiteral("true"));
} else if (attr.name() == QStringLiteral("state")) {
state = ProxyManager::ProxyStateFromString(
attr.value().toString());
} else if (attr.name() == QStringLiteral("stream")) {
stream = attr.value().toInt();
} else if (attr.name() == QStringLiteral("preset")) {
preset_version = attr.value().toInt();
}
}
}
const QString path = reader->readElementText();
if (!path.isEmpty()) {
SetProxy(path, state, stream, preset_version, enabled);
}
} else if (reader->name() == QStringLiteral("sourcestarttime")) {
QString source;
{
@@ -512,6 +572,22 @@ void Footage::SaveCustom(QXmlStreamWriter *writer) const
writer->writeTextElement(QStringLiteral("timestamp"),
QString::number(this->timestamp()));
if (!proxy_path_.isEmpty()) {
writer->writeStartElement(QStringLiteral("proxy"));
writer->writeAttribute(QStringLiteral("enabled"),
proxy_enabled_ ? QStringLiteral("1")
: QStringLiteral("0"));
writer->writeAttribute(QStringLiteral("state"),
ProxyManager::ProxyStateToString(
proxy_state_));
writer->writeAttribute(QStringLiteral("stream"),
QString::number(proxy_video_stream_index_));
writer->writeAttribute(QStringLiteral("preset"),
QString::number(proxy_preset_version_));
writer->writeCharacters(proxy_path_);
writer->writeEndElement();
}
if (has_source_start_time_) {
writer->writeStartElement(QStringLiteral("sourcestarttime"));
writer->writeAttribute(QStringLiteral("source"),
@@ -533,12 +609,24 @@ void Footage::AddedToGraphEvent(Project *p)
{
connect(p->color_manager(), &ColorManager::DefaultInputChanged, this,
&Footage::DefaultColorSpaceChanged);
if (ProxyManager::instance()) {
connect(ProxyManager::instance(), &ProxyManager::ProxyReady, this,
&Footage::ProxyReady);
connect(ProxyManager::instance(), &ProxyManager::ProxyFinished, this,
&Footage::ProxyFinished);
}
}
void Footage::RemovedFromGraphEvent(Project *p)
{
disconnect(p->color_manager(), &ColorManager::DefaultInputChanged, this,
&Footage::DefaultColorSpaceChanged);
if (ProxyManager::instance()) {
disconnect(ProxyManager::instance(), &ProxyManager::ProxyReady, this,
&Footage::ProxyReady);
disconnect(ProxyManager::instance(), &ProxyManager::ProxyFinished, this,
&Footage::ProxyFinished);
}
}
void Footage::Reprobe()
@@ -706,4 +794,25 @@ void Footage::DefaultColorSpaceChanged()
}
}
void Footage::ProxyReady(const QString &source_filename, int stream_index,
const QString &proxy_filename)
{
ProxyFinished(source_filename, stream_index, proxy_filename,
ProxyManager::kProxyReady);
}
void Footage::ProxyFinished(const QString &source_filename, int stream_index,
const QString &proxy_filename,
ProxyManager::ProxyState state)
{
if (filename() != source_filename ||
proxy_video_stream_index_ != stream_index ||
proxy_path_ != proxy_filename) {
return;
}
proxy_state_ = state;
InvalidateAll(kFilenameInput);
}
}
+55
View File
@@ -27,6 +27,7 @@
#include <QDateTime>
#include "codec/decoder.h"
#include "codec/proxymanager.h"
#include "footagedescription.h"
#include "node/output/viewer/viewer.h"
#include "render/cancelatom.h"
@@ -172,6 +173,44 @@ public:
void SetSourceStartTime(const rational &time, const QString &source);
bool proxy_enabled() const
{
return proxy_enabled_;
}
void set_proxy_enabled(bool enabled)
{
proxy_enabled_ = enabled;
}
const QString &proxy_path() const
{
return proxy_path_;
}
int proxy_video_stream_index() const
{
return proxy_video_stream_index_;
}
int proxy_preset_version() const
{
return proxy_preset_version_;
}
ProxyManager::ProxyState proxy_state() const
{
return proxy_state_;
}
void SetProxy(const QString &path,
ProxyManager::ProxyState state,
int video_stream_index,
int preset_version,
bool enabled);
void ClearProxy();
static QString DescribeVideoStream(const VideoParams &params);
static QString DescribeAudioStream(const AudioParams &params);
static QString DescribeSubtitleStream(const SubtitleParams &params);
@@ -236,6 +275,16 @@ private:
bool has_source_start_time_;
bool proxy_enabled_;
QString proxy_path_;
ProxyManager::ProxyState proxy_state_;
int proxy_video_stream_index_;
int proxy_preset_version_;
bool valid_;
CancelAtom *cancelled_;
@@ -246,6 +295,12 @@ private slots:
void CheckFootage();
void DefaultColorSpaceChanged();
void ProxyReady(const QString &source_filename, int stream_index,
const QString &proxy_filename);
void ProxyFinished(const QString &source_filename, int stream_index,
const QString &proxy_filename,
ProxyManager::ProxyState state);
};
}
+37
View File
@@ -56,6 +56,35 @@ public:
return filename_;
}
bool has_proxy() const
{
return has_proxy_;
}
const QString &proxy_filename() const
{
return proxy_filename_;
}
const QString &proxy_decoder() const
{
return proxy_decoder_;
}
int proxy_stream_index() const
{
return proxy_stream_index_;
}
void set_proxy(const QString &filename, const QString &decoder,
int stream_index)
{
proxy_filename_ = filename;
proxy_decoder_ = decoder;
proxy_stream_index_ = stream_index;
has_proxy_ = !filename.isEmpty();
}
Track::Type type() const
{
return type_;
@@ -122,6 +151,14 @@ private:
QString filename_;
bool has_proxy_ = false;
QString proxy_filename_;
QString proxy_decoder_;
int proxy_stream_index_ = -1;
Track::Type type_;
VideoParams video_params_;
+15 -5
View File
@@ -21,6 +21,7 @@
#include "renderprocessor.h"
#include <QFileInfo>
#include <QOpenGLContext>
#include <QVector2D>
#include <QVector3D>
@@ -501,10 +502,19 @@ void RenderProcessor::ProcessVideoFootage(TexturePtr destination,
return;
}
Decoder::CodecStream default_codec_stream(
stream->filename(), stream_data.stream_index(), GetCurrentBlock());
const bool use_proxy =
static_cast<RenderMode::Mode>(ticket_->property("mode").toInt()) ==
RenderMode::kOffline &&
stream->has_proxy() && QFileInfo::exists(stream->proxy_filename());
const QString decode_filename =
use_proxy ? stream->proxy_filename() : stream->filename();
const QString decoder_id =
use_proxy ? stream->proxy_decoder() : stream->decoder();
const int stream_index =
use_proxy ? stream->proxy_stream_index() : stream_data.stream_index();
QString decoder_id = stream->decoder();
Decoder::CodecStream default_codec_stream(
decode_filename, stream_index, GetCurrentBlock());
DecoderPtr decoder = nullptr;
@@ -523,11 +533,11 @@ void RenderProcessor::ProcessVideoFootage(TexturePtr destination,
int64_t frame_number =
stream_data.get_time_in_timebase_units(input_time);
frame_filename = Decoder::TransformImageSequenceFileName(
stream->filename(), frame_number);
decode_filename, frame_number);
// Decoder will close automatically since it's a stream_ptr
decoder->Open(Decoder::CodecStream(
frame_filename, stream_data.stream_index(), GetCurrentBlock()));
frame_filename, stream_index, GetCurrentBlock()));
}
break;
}
+1
View File
@@ -19,6 +19,7 @@ add_subdirectory(customcache)
add_subdirectory(export)
add_subdirectory(precache)
add_subdirectory(project)
add_subdirectory(proxy)
add_subdirectory(render)
set(OLIVE_SOURCES
+9
View File
@@ -0,0 +1,9 @@
# Oak Video Editor - Non-Linear Video Editor
# Copyright (C) 2026 Oak Team
set(OLIVE_SOURCES
${OLIVE_SOURCES}
task/proxy/proxy.h
task/proxy/proxy.cpp
PARENT_SCOPE
)
+114
View File
@@ -0,0 +1,114 @@
/*
* 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 "proxy.h"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QProcess>
#include <QStandardPaths>
namespace olive
{
ProxyTask::ProxyTask(const QString &source_filename,
int stream_index,
const ProxyManager::ProxyParams &params,
const QString &output_filename)
: source_filename_(source_filename)
, stream_index_(stream_index)
, params_(params)
, output_filename_(output_filename)
{
SetTitle(tr("Generating Proxy %1:%2")
.arg(source_filename_, QString::number(stream_index_)));
}
bool ProxyTask::Run()
{
const QString ffmpeg = QStandardPaths::findExecutable(QStringLiteral("ffmpeg"));
if (ffmpeg.isEmpty()) {
SetError(tr("Failed to generate proxy: ffmpeg executable was not found"));
return false;
}
QDir output_dir = QFileInfo(output_filename_).dir();
if (!output_dir.exists() && !output_dir.mkpath(QStringLiteral("."))) {
SetError(tr("Failed to create proxy output directory"));
return false;
}
QFile::remove(output_filename_);
const QString scale_filter = QStringLiteral(
"scale=w=%1:h=%2:force_original_aspect_ratio=decrease")
.arg(QString::number(params_.width),
QString::number(params_.height));
QStringList args;
args << QStringLiteral("-y")
<< QStringLiteral("-i") << source_filename_
<< QStringLiteral("-map") << QStringLiteral("0:%1").arg(stream_index_)
<< QStringLiteral("-an")
<< QStringLiteral("-vf") << scale_filter
<< QStringLiteral("-c:v") << QStringLiteral("libx264")
<< QStringLiteral("-preset") << QStringLiteral("veryfast")
<< QStringLiteral("-crf") << QStringLiteral("23")
<< QStringLiteral("-pix_fmt") << QStringLiteral("yuv420p")
<< QStringLiteral("-movflags") << QStringLiteral("+faststart")
<< output_filename_;
QProcess process;
process.setProgram(ffmpeg);
process.setArguments(args);
process.setProcessChannelMode(QProcess::MergedChannels);
process.start();
if (!process.waitForStarted()) {
SetError(tr("Failed to start ffmpeg for proxy generation"));
return false;
}
while (!process.waitForFinished(100)) {
if (IsCancelled()) {
process.kill();
process.waitForFinished();
QFile::remove(output_filename_);
SetError(tr("Proxy generation was cancelled"));
return false;
}
}
if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
const QString output = QString::fromUtf8(process.readAll()).trimmed();
QFile::remove(output_filename_);
SetError(tr("ffmpeg failed to generate proxy: %1").arg(output));
return false;
}
if (!QFileInfo::exists(output_filename_)) {
SetError(tr("ffmpeg finished but proxy file was not created"));
return false;
}
emit ProgressChanged(1.0);
return true;
}
}
+48
View File
@@ -0,0 +1,48 @@
/*
* 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 PROXYTASK_H
#define PROXYTASK_H
#include "codec/proxymanager.h"
#include "task/task.h"
namespace olive
{
class ProxyTask : public Task {
Q_OBJECT
public:
ProxyTask(const QString &source_filename,
int stream_index,
const ProxyManager::ProxyParams &params,
const QString &output_filename);
protected:
virtual bool Run() override;
private:
QString source_filename_;
int stream_index_;
ProxyManager::ProxyParams params_;
QString output_filename_;
};
}
#endif // PROXYTASK_H
@@ -24,12 +24,19 @@
#include <algorithm>
#include <cfloat>
#include <cmath>
#include <QDesktopServices>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QProcess>
#include <QSplitter>
#include <QUrl>
#include <QVBoxLayout>
#include <QtMath>
#include "audio/audiosynchronizer.h"
#include "audio/audiowaveformsync.h"
#include "codec/proxymanager.h"
#include "core.h"
#include "common/range.h"
#include "dialog/sequence/sequence.h"
@@ -155,6 +162,26 @@ QVector<WaveformSyncClip> GetSelectedWaveformSyncClips(
return clips;
}
QVector<Footage *> GetSelectedProxyFootage(const QVector<Block *> &blocks)
{
QVector<Footage *> footage;
for (Block *block : blocks) {
ClipBlock *clip = dynamic_cast<ClipBlock *>(block);
if (!clip) {
continue;
}
Footage *candidate = dynamic_cast<Footage *>(clip->connected_viewer());
if (!candidate || !candidate->GetFirstEnabledVideoStream().is_valid() ||
footage.contains(candidate)) {
continue;
}
footage.append(candidate);
}
return footage;
}
QVector<double> ExtractWaveformCacheEnvelope(const WaveformSyncClip &clip,
int sample_rate,
size_t window_samples)
@@ -1152,6 +1179,90 @@ void TimelineWidget::SynchronizeSelectedClipsByWaveform()
command, tr("Synchronize Clips by Waveform"));
}
void TimelineWidget::GenerateProxiesForSelectedClips()
{
if (!ProxyManager::instance() || !sequence()) {
return;
}
const QVector<Footage *> footage = GetSelectedProxyFootage(selected_blocks_);
for (Footage *item : footage) {
const VideoParams video = item->GetFirstEnabledVideoStream();
if (!video.is_valid()) {
continue;
}
ProxyManager::ProxyParams params;
const ProxyManager::Proxy proxy =
ProxyManager::instance()->GetOrStartProxy(
item->project()->cache_path(), item->filename(),
video.stream_index(), params);
item->SetProxy(proxy.filename, proxy.state, video.stream_index(),
params.version, true);
item->InvalidateAll(Footage::kFilenameInput);
}
}
void TimelineWidget::SetSelectedClipsProxyEnabled(bool enabled)
{
const QVector<Footage *> footage = GetSelectedProxyFootage(selected_blocks_);
for (Footage *item : footage) {
if (item->proxy_path().isEmpty()) {
continue;
}
item->set_proxy_enabled(enabled);
item->InvalidateAll(Footage::kFilenameInput);
}
}
void TimelineWidget::RevealProxyForSelectedClips()
{
const QVector<Footage *> footage = GetSelectedProxyFootage(selected_blocks_);
for (Footage *item : footage) {
if (item->proxy_path().isEmpty()) {
continue;
}
#if defined(Q_OS_WINDOWS)
QStringList args;
args << "/select," << QDir::toNativeSeparators(item->proxy_path());
QProcess::startDetached(QStringLiteral("explorer"), args);
#elif defined(Q_OS_MAC)
QStringList args;
args << "-e";
args << "tell application \"Finder\"";
args << "-e";
args << "activate";
args << "-e";
args << "select POSIX file \"" + item->proxy_path() + "\"";
args << "-e";
args << "end tell";
QProcess::startDetached(QStringLiteral("osascript"), args);
#else
QDesktopServices::openUrl(QUrl::fromLocalFile(
QFileInfo(item->proxy_path()).dir().absolutePath()));
#endif
return;
}
}
void TimelineWidget::DeleteProxiesForSelectedClips()
{
const QVector<Footage *> footage = GetSelectedProxyFootage(selected_blocks_);
for (Footage *item : footage) {
if (item->proxy_path().isEmpty()) {
continue;
}
QFile::remove(item->proxy_path());
QFile::remove(ProxyManager::GetWorkingProxyFilename(
item->proxy_path()));
item->ClearProxy();
item->InvalidateAll(Footage::kFilenameInput);
}
}
void TimelineWidget::RecordingCallback(const QString &filename,
const TimeRange &time,
const Track::Reference &track)
@@ -1653,6 +1764,51 @@ void TimelineWidget::ShowContextMenu()
&TimelineWidget::CacheDiscard);
}
{
const QVector<Footage *> proxy_footage =
GetSelectedProxyFootage(selected);
Menu *proxy_menu = new Menu(tr("Proxy"), &menu);
menu.addMenu(proxy_menu);
QAction *generate_proxy =
proxy_menu->addAction(tr("Generate Proxy"));
generate_proxy->setEnabled(!proxy_footage.isEmpty());
connect(generate_proxy, &QAction::triggered, this,
&TimelineWidget::GenerateProxiesForSelectedClips);
QAction *use_proxy = proxy_menu->addAction(tr("Use Proxy"));
use_proxy->setCheckable(true);
use_proxy->setEnabled(!proxy_footage.isEmpty());
use_proxy->setChecked(
!proxy_footage.isEmpty() &&
std::all_of(proxy_footage.cbegin(), proxy_footage.cend(),
[](const Footage *footage) {
return footage->proxy_enabled();
}));
connect(use_proxy, &QAction::triggered, this,
&TimelineWidget::SetSelectedClipsProxyEnabled);
QAction *reveal_proxy =
proxy_menu->addAction(tr("Reveal Proxy"));
reveal_proxy->setEnabled(std::any_of(
proxy_footage.cbegin(), proxy_footage.cend(),
[](const Footage *footage) {
return !footage->proxy_path().isEmpty();
}));
connect(reveal_proxy, &QAction::triggered, this,
&TimelineWidget::RevealProxyForSelectedClips);
QAction *delete_proxy =
proxy_menu->addAction(tr("Delete Proxy"));
delete_proxy->setEnabled(std::any_of(
proxy_footage.cbegin(), proxy_footage.cend(),
[](const Footage *footage) {
return !footage->proxy_path().isEmpty();
}));
connect(delete_proxy, &QAction::triggered, this,
&TimelineWidget::DeleteProxiesForSelectedClips);
}
if (clip->connected_viewer()) {
QAction *reveal_in_footage_viewer =
menu.addAction(tr("Reveal in Footage Viewer"));
@@ -109,6 +109,14 @@ public:
void SynchronizeSelectedClipsByWaveform();
void GenerateProxiesForSelectedClips();
void SetSelectedClipsProxyEnabled(bool enabled);
void RevealProxyForSelectedClips();
void DeleteProxiesForSelectedClips();
void RecordingCallback(const QString &filename, const TimeRange &time,
const Track::Reference &track);